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

Implement support for robust color limits #4712

Merged
merged 7 commits into from
Nov 30, 2020
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
3 changes: 2 additions & 1 deletion examples/user_guide/04-Style_Mapping.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,8 @@
" \n",
"#### Other options\n",
"\n",
"* ``logz``: Enable logarithmic color scale (e.g. ``logz=True``)\n",
"* ``clim_percentile``: Percentile value to compute colorscale robust to outliers. If `True` uses 2nd and 98th percentile, otherwise uses the specified percentile value. \n",
"* ``cnorm``: Color normalization to be applied during colormapping. Allows switching between 'linear', 'log' and 'eqhist'.\n",
"* ``symmetric``: Ensures that the color scale is centered on zero (e.g. ``symmetric=True``)"
]
},
Expand Down
12 changes: 10 additions & 2 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -1700,6 +1700,11 @@ class ColorbarPlot(ElementPlot):
User-specified colorbar axis range limits for the plot, as a tuple (low,high).
If specified, takes precedence over data and dimension ranges.""")

clim_percentile = param.ClassSelector(default=False, class_=(int, float, bool), doc="""
Percentile value to compute colorscale robust to outliers. If
True uses 2nd and 98th percentile, otherwise uses the specified
percentile value.""")

cformatter = param.ClassSelector(
default=None, class_=(util.basestring, TickFormatter, FunctionType), doc="""
Formatter for ticks along the colorbar axis.""")
Expand Down Expand Up @@ -1727,7 +1732,7 @@ class ColorbarPlot(ElementPlot):
#FFFFFFFF or a length 3 or length 4 tuple specifying values in
the range 0-1 or a named HTML color.""")

logz = param.Boolean(default=False, doc="""
logz = param.Boolean(default=False, doc="""
Whether to apply log scaling to the z-axis.""")

symmetric = param.Boolean(default=False, doc="""
Expand Down Expand Up @@ -1812,7 +1817,10 @@ def _get_colormapper(self, eldim, element, ranges, style, factors=None, colors=N
if all(util.isfinite(cl) for cl in self.clim):
low, high = self.clim
elif dim_name in ranges:
low, high = ranges[dim_name]['combined']
if self.clim_percentile and 'robust' in ranges[dim_name]:
low, high = ranges[dim_name]['robust']
else:
low, high = ranges[dim_name]['combined']
dlow, dhigh = ranges[dim_name]['data']
if (util.is_int(low, int_like=True) and
util.is_int(high, int_like=True) and
Expand Down
15 changes: 12 additions & 3 deletions holoviews/plotting/mpl/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,8 +666,14 @@ class ColorbarPlot(ElementPlot):
over the title key in colorbar_opts.""")

clim = param.NumericTuple(default=(np.nan, np.nan), length=2, doc="""
User-specified colorbar axis range limits for the plot, as a tuple (low,high).
If specified, takes precedence over data and dimension ranges.""")
User-specified colorbar axis range limits for the plot, as a
tuple (low,high). If specified, takes precedence over data
and dimension ranges.""")

clim_percentile = param.ClassSelector(default=False, class_=(int, float, bool), doc="""
Percentile value to compute colorscale robust to outliers. If
True uses 2nd and 98th percentile, otherwise uses the specified
percentile value.""")

cformatter = param.ClassSelector(
default=None, class_=(util.basestring, ticker.Formatter, FunctionType), doc="""
Expand Down Expand Up @@ -850,7 +856,10 @@ def _norm_kwargs(self, element, ranges, opts, vdim, values=None, prefix=''):
categorical = False
elif values.dtype.kind in 'uif':
if dim_name in ranges:
clim = ranges[dim_name]['combined']
if self.clim_percentile and 'robust' in ranges[dim_name]:
clim = ranges[dim_name]['robust']
else:
clim = ranges[dim_name]['combined']
elif isinstance(vdim, dim):
if values.dtype.kind == 'M':
clim = values.min(), values.max()
Expand Down
32 changes: 24 additions & 8 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ def compute_ranges(self, obj, key, ranges):
# at this level, and ranges for the group have not
# been supplied from a composite plot
return_fn = lambda x: x if isinstance(x, Element) else None
for group, (axiswise, framewise) in norm_opts.items():
for group, (axiswise, framewise, robust) in norm_opts.items():
axiswise = (not getattr(self, 'shared_axes', True)) or (axiswise)
elements = []
# Skip if ranges are cached or already computed by a
Expand All @@ -634,7 +634,8 @@ def compute_ranges(self, obj, key, ranges):
# or not framewise on a Overlay or ElementPlot
if (not (axiswise and not isinstance(obj, HoloMap)) or
(not framewise and isinstance(obj, HoloMap))):
self._compute_group_range(group, elements, ranges, framewise, self.top_level)
self._compute_group_range(group, elements, ranges, framewise,
robust, self.top_level)
self.ranges.update(ranges)
return ranges

Expand Down Expand Up @@ -676,21 +677,25 @@ def _get_norm_opts(self, obj):
for i in range(1, 4))
if applies and 'norm' in opts.groups:
nopts = opts['norm'].options
if 'axiswise' in nopts or 'framewise' in nopts:
popts = opts['plot'].options
if 'axiswise' in nopts or 'framewise' in nopts or 'clim_percentile' in popts:
norm_opts.update({path: (nopts.get('axiswise', False),
nopts.get('framewise', False))})
nopts.get('framewise', False),
popts.get('clim_percentile', False))})
element_specs = [spec for _, spec in element_specs]
norm_opts.update({spec: (False, False) for spec in element_specs
norm_opts.update({spec: (False, False, False) for spec in element_specs
if not any(spec[:i] in norm_opts.keys() for i in range(1, 4))})
return norm_opts


@classmethod
def _compute_group_range(cls, group, elements, ranges, framewise, top_level):
def _compute_group_range(cls, group, elements, ranges, framewise, robust, top_level):
# Iterate over all elements in a normalization group
# and accumulate their ranges into the supplied dictionary.
elements = [el for el in elements if el is not None]

data_ranges = {}
robust_ranges = {}
categorical_dims = []
for el in elements:
for el_dim in el.dimensions('ranges'):
Expand Down Expand Up @@ -719,6 +724,12 @@ def _compute_group_range(cls, group, elements, ranges, framewise, top_level):
data_range = el.range(el_dim, dimension_range=False)

data_ranges[(el, el_dim)] = data_range
if dtype is not None and dtype.kind == 'uif' and robust:
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
percentile = 2 if isinstance(robust, bool) else robust
robust_ranges[(el, el_dim)] = (
dim(el_dim, np.nanpercentile, percentile).apply(el),
dim(el_dim, np.nanpercentile, percentile).apply(el)
)

if (any(isinstance(r, util.basestring) for r in data_range) or
(el_dim.type is not None and issubclass(el_dim.type, util.basestring)) or
Expand Down Expand Up @@ -775,10 +786,13 @@ def _compute_group_range(cls, group, elements, ranges, framewise, top_level):
continue
data_range = data_ranges[(el, el_dim)]
if dim_name not in group_ranges:
group_ranges[dim_name] = {'data': [], 'hard': [], 'soft': []}
group_ranges[dim_name] = {'data': [], 'hard': [],
'soft': [], 'robust': []}
group_ranges[dim_name]['data'].append(data_range)
group_ranges[dim_name]['hard'].append(el_dim.range)
group_ranges[dim_name]['soft'].append(el_dim.soft_range)
if (el, el_dim) in robust_ranges:
group_ranges[dim_name]['robust'].append(robust_ranges[(el, el_dim)])
if el_dim in categorical_dims:
if 'factors' not in group_ranges[dim_name]:
group_ranges[dim_name]['factors'] = []
Expand Down Expand Up @@ -815,11 +829,13 @@ def _compute_group_range(cls, group, elements, ranges, framewise, top_level):
for gdim, values in group_dim_ranges.items():
hard_range = util.max_range(values['hard'], combined=False)
soft_range = util.max_range(values['soft'])
robust_range = util.max_range(values.get('robust', []))
data_range = util.max_range(values['data'])
combined = util.dimension_range(data_range[0], data_range[1],
hard_range, soft_range)
dranges = {'data': data_range, 'hard': hard_range,
'soft': soft_range, 'combined': combined}
'soft': soft_range, 'combined': combined,
'robust': robust_range}
if 'factors' in values:
all_factors = values['factors']
factor_dtypes = {fs.dtype for fs in all_factors} if all_factors else []
Expand Down
15 changes: 12 additions & 3 deletions holoviews/plotting/plotly/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,14 @@ def update_frame(self, key, ranges=None, element=None, is_geo=False):
class ColorbarPlot(ElementPlot):

clim = param.NumericTuple(default=(np.nan, np.nan), length=2, doc="""
User-specified colorbar axis range limits for the plot, as a tuple (low,high).
If specified, takes precedence over data and dimension ranges.""")
User-specified colorbar axis range limits for the plot, as a
tuple (low,high). If specified, takes precedence over data
and dimension ranges.""")

clim_percentile = param.ClassSelector(default=False, class_=(int, float, bool), doc="""
Percentile value to compute colorscale robust to outliers. If
True uses 2nd and 98th percentile, otherwise uses the specified
percentile value.""")

colorbar = param.Boolean(default=False, doc="""
Whether to display a colorbar.""")
Expand Down Expand Up @@ -608,7 +614,10 @@ def get_color_opts(self, eldim, element, ranges, style):
if util.isfinite(self.clim).all():
cmin, cmax = self.clim
elif dim_name in ranges:
cmin, cmax = ranges[dim_name]['combined']
if self.clim_percentile and 'robust' in ranges[dim_name]:
low, high = ranges[dim_name]['robust']
else:
cmin, cmax = ranges[dim_name]['combined']
elif isinstance(eldim, dim):
cmin, cmax = np.nan, np.nan
auto = True
Expand Down