From 63bbabc29b2773fb60982df4a90f75d196900e96 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 1 Jan 2024 13:27:20 -0800 Subject: [PATCH 01/24] apply hard navigable bounds by default --- holoviews/plotting/bokeh/element.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 874132b499..79b9d1dd54 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -105,6 +105,10 @@ class ElementPlot(BokehPlot, GenericElementPlot): align = param.ObjectSelector(default='start', objects=['start', 'center', 'end'], doc=""" Alignment (vertical or horizontal) of the plot in a layout.""") + apply_hard_bounds = param.Boolean(default=True, doc=""" + If True, the navigable bounds of the plot will be set based + on the extents of the data. If False, the bounds will not be set.""") + autorange = param.ObjectSelector(default=None, objects=['x', 'y', None], doc=""" Whether to auto-range along either the x- or y-axis, i.e. when panning or zooming along the orthogonal axis it will @@ -1894,6 +1898,10 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): if self._subcoord_overlaid: if style_element.label in plot.extra_y_ranges: self.handles['y_range'] = plot.extra_y_ranges.pop(style_element.label) + + if self.apply_hard_bounds: + self._apply_hard_bound(element, ranges) + self.handles['plot'] = plot if self.autorange: @@ -1919,6 +1927,13 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): return plot + def _apply_hard_bound(self, element, ranges): + # Set the navigable bounds + xmin, ymin, xmax, ymax = self.get_extents(element, ranges) + if not all(np.isnan([xmin, ymin, xmax, ymax])): + self.handles['x_range'].bounds = (xmin, xmax) + self.handles['y_range'].bounds = (ymin, ymax) + def _setup_data_callbacks(self, plot): if not self._js_on_data_callbacks: return From 2acd4f875fd5b748fecac8494638a9cdd7a0fa36 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 1 Jan 2024 17:21:10 -0800 Subject: [PATCH 02/24] skip if no extents and use None if identical --- holoviews/plotting/bokeh/element.py | 33 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 79b9d1dd54..2c282b5492 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1928,11 +1928,34 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): return plot def _apply_hard_bound(self, element, ranges): - # Set the navigable bounds - xmin, ymin, xmax, ymax = self.get_extents(element, ranges) - if not all(np.isnan([xmin, ymin, xmax, ymax])): - self.handles['x_range'].bounds = (xmin, xmax) - self.handles['y_range'].bounds = (ymin, ymax) + """ + Apply hard bounds to the x and y ranges of the plot. + + Sets the navigable bounds of the plot based on the extents + of the given element and ranges. If an extend is numeric and not NaN, it is + used as is. Otherwise, it is set to None, which means that end of the axis + is unbounded. + """ + + # Skip if the element doesn't have an 'extents' attribute + if not hasattr(element, 'extents'): + return + + def validate_bound(bound): + """Validate a single bound, returning None if it is not a valid number""" + return bound if isinstance(bound, (int, float)) and not np.isnan(bound) else None + + min_extent_x, min_extent_y, max_extent_x, max_extent_y = map(validate_bound, self.get_extents(element, ranges)) + + def set_bounds(axis, min_extent, max_extent): + """Set the bounds for a given axis, using None if both extents are None or identical""" + if min_extent == max_extent: + self.handles[axis].bounds = None + else: + self.handles[axis].bounds = (min_extent, max_extent) if min_extent is not None or max_extent is not None else None + + set_bounds('x_range', min_extent_x, max_extent_x) + set_bounds('y_range', min_extent_y, max_extent_y) def _setup_data_callbacks(self, plot): if not self._js_on_data_callbacks: From 30af8a0bc39e2e55f11c3c880338af88202c527f Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 2 Jan 2024 17:45:48 -0800 Subject: [PATCH 03/24] rebase on main... disable hard bounds for test_multi_axis_rangexy --- holoviews/tests/ui/bokeh/test_callback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index f08e056744..e577424440 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -127,9 +127,9 @@ def test_rangexy(serve_hv): wait_until(lambda: rangexy.x_range == expected_xrange and rangexy.y_range == expected_yrange, page) @pytest.mark.usefixtures("bokeh_backend") -def test_multi_axis_rangexy(serve_hv): - c1 = Curve(np.arange(100).cumsum(), vdims='y') - c2 = Curve(-np.arange(100).cumsum(), vdims='y2') +def test_multi_axis_rangexy(page, port): + c1 = Curve(np.arange(100).cumsum(), vdims='y').opts(apply_hard_bounds=False) + c2 = Curve(-np.arange(100).cumsum(), vdims='y2').opts(apply_hard_bounds=False) s1 = RangeXY(source=c1) s2 = RangeXY(source=c2) From ccef3ca8c3def2af683a644d27a2188278697f9d Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 2 Apr 2024 13:14:17 -0700 Subject: [PATCH 04/24] Update holoviews/plotting/bokeh/element.py Co-authored-by: Philipp Rudiger --- holoviews/plotting/bokeh/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 2c282b5492..e3dd7490a2 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1932,7 +1932,7 @@ def _apply_hard_bound(self, element, ranges): Apply hard bounds to the x and y ranges of the plot. Sets the navigable bounds of the plot based on the extents - of the given element and ranges. If an extend is numeric and not NaN, it is + of the given element and ranges. If an extent is numeric and not NaN, it is used as is. Otherwise, it is set to None, which means that end of the axis is unbounded. """ From c3da8f611fc8d36bb036e0fa808c77ae7dd9ab2d Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 8 Apr 2024 15:54:53 -0700 Subject: [PATCH 05/24] allow none --- holoviews/plotting/bokeh/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e3dd7490a2..9f1bd4fd82 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -105,7 +105,7 @@ class ElementPlot(BokehPlot, GenericElementPlot): align = param.ObjectSelector(default='start', objects=['start', 'center', 'end'], doc=""" Alignment (vertical or horizontal) of the plot in a layout.""") - apply_hard_bounds = param.Boolean(default=True, doc=""" + apply_hard_bounds = param.Boolean(default=True, allow_None=True, doc=""" If True, the navigable bounds of the plot will be set based on the extents of the data. If False, the bounds will not be set.""") From 77d2f31d8590b7892ba5f4dfae4276522f7218ca Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 8 Apr 2024 16:35:51 -0700 Subject: [PATCH 06/24] apply hard bounds by default if projection is unset --- holoviews/plotting/bokeh/element.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 9f1bd4fd82..b48ba54ae4 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -105,7 +105,7 @@ class ElementPlot(BokehPlot, GenericElementPlot): align = param.ObjectSelector(default='start', objects=['start', 'center', 'end'], doc=""" Alignment (vertical or horizontal) of the plot in a layout.""") - apply_hard_bounds = param.Boolean(default=True, allow_None=True, doc=""" + apply_hard_bounds = param.Boolean(default=None, allow_None=True, doc=""" If True, the navigable bounds of the plot will be set based on the extents of the data. If False, the bounds will not be set.""") @@ -1899,8 +1899,10 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): if style_element.label in plot.extra_y_ranges: self.handles['y_range'] = plot.extra_y_ranges.pop(style_element.label) - if self.apply_hard_bounds: - self._apply_hard_bound(element, ranges) + # If apply_hard_bound is not set but projection has been set, avoid applying hard bounds by default + # since a projection typically means that the plot is either geographic, polar, or 3d + if self.apply_hard_bounds or (self.projection is None and self.apply_hard_bounds is None): + self._apply_hard_bounds(element, ranges) self.handles['plot'] = plot @@ -1927,7 +1929,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): return plot - def _apply_hard_bound(self, element, ranges): + def _apply_hard_bounds(self, element, ranges): """ Apply hard bounds to the x and y ranges of the plot. From d6b87839eb3f3d088f38df916face8e3471b4888 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 8 Apr 2024 18:33:01 -0700 Subject: [PATCH 07/24] add flag to use lims as soft ranges --- holoviews/plotting/plot.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 612091659c..3cfc2517c1 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -1423,7 +1423,7 @@ def _get_range_extents(self, element, ranges, range_type, xdim, ydim, zdim): return (x0, y0, x1, y1) - def get_extents(self, element, ranges, range_type='combined', dimension=None, xdim=None, ydim=None, zdim=None, **kwargs): + def get_extents(self, element, ranges, range_type='combined', dimension=None, xdim=None, ydim=None, zdim=None, lims_as_soft_ranges=False, **kwargs): """ Gets the extents for the axes from the current Element. The globally computed ranges can optionally override the extents. @@ -1444,6 +1444,11 @@ def get_extents(self, element, ranges, range_type='combined', dimension=None, xd This allows Overlay plots to obtain each range and combine them appropriately for all the objects in the overlay. + + If lims_as_soft_ranges is set to True, the xlim and ylim will be treated as + soft ranges instead of the default case as hard ranges while computing the extents. + This is useful when computing the maximum extents across data, padding, xlim/ylim, + and dimension ranges. """ num = 6 if (isinstance(self.projection, str) and self.projection == '3d') else 4 if self.apply_extents and range_type in ('combined', 'extents'): @@ -1486,8 +1491,12 @@ def get_extents(self, element, ranges, range_type='combined', dimension=None, xd else: x0, y0, x1, y1 = combined - x0, x1 = util.dimension_range(x0, x1, self.xlim, (None, None)) - y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) + if lims_as_soft_ranges: + x0, x1 = util.dimension_range(x0, x1, (None, None), self.xlim) + y0, y1 = util.dimension_range(y0, y1, (None, None), self.ylim) + else: + x0, x1 = util.dimension_range(x0, x1, self.xlim, (None, None)) + y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) if not self.drawn: x_range, y_range = ((y0, y1), (x0, x1)) if self.invert_axes else ((x0, x1), (y0, y1)) From 8d167e55ea05b79eeee24e64bf40446c10cab31d Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 8 Apr 2024 18:33:52 -0700 Subject: [PATCH 08/24] default to false --- holoviews/plotting/bokeh/element.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index b48ba54ae4..562723c7a2 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -105,9 +105,9 @@ class ElementPlot(BokehPlot, GenericElementPlot): align = param.ObjectSelector(default='start', objects=['start', 'center', 'end'], doc=""" Alignment (vertical or horizontal) of the plot in a layout.""") - apply_hard_bounds = param.Boolean(default=None, allow_None=True, doc=""" + apply_hard_bounds = param.Boolean(default=False, doc=""" If True, the navigable bounds of the plot will be set based - on the extents of the data. If False, the bounds will not be set.""") + on the larger of extents of the data+padding, xlim/ylim, dim ranges.""") autorange = param.ObjectSelector(default=None, objects=['x', 'y', None], doc=""" Whether to auto-range along either the x- or y-axis, i.e. @@ -1939,15 +1939,11 @@ def _apply_hard_bounds(self, element, ranges): is unbounded. """ - # Skip if the element doesn't have an 'extents' attribute - if not hasattr(element, 'extents'): - return - def validate_bound(bound): """Validate a single bound, returning None if it is not a valid number""" - return bound if isinstance(bound, (int, float)) and not np.isnan(bound) else None + return bound if util.isfinite(bound) else None - min_extent_x, min_extent_y, max_extent_x, max_extent_y = map(validate_bound, self.get_extents(element, ranges)) + min_extent_x, min_extent_y, max_extent_x, max_extent_y = map(validate_bound, self.get_extents(element, ranges, lims_as_soft_ranges=True)) def set_bounds(axis, min_extent, max_extent): """Set the bounds for a given axis, using None if both extents are None or identical""" From 54ff9ae07aec0db1346c98aa0ed6d838c0f96804 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 8 Apr 2024 18:37:47 -0700 Subject: [PATCH 09/24] simplify conditional since default now false --- holoviews/plotting/bokeh/element.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 562723c7a2..21a81d0e64 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1899,9 +1899,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): if style_element.label in plot.extra_y_ranges: self.handles['y_range'] = plot.extra_y_ranges.pop(style_element.label) - # If apply_hard_bound is not set but projection has been set, avoid applying hard bounds by default - # since a projection typically means that the plot is either geographic, polar, or 3d - if self.apply_hard_bounds or (self.projection is None and self.apply_hard_bounds is None): + if self.apply_hard_bounds: self._apply_hard_bounds(element, ranges) self.handles['plot'] = plot From 5eff4d8307f464a132cedb29b499594151924101 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Thu, 11 Apr 2024 12:33:57 -0700 Subject: [PATCH 10/24] clean up description --- holoviews/plotting/bokeh/element.py | 12 +++++------- holoviews/plotting/plot.py | 5 +++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 21a81d0e64..af1f996e2c 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1929,19 +1929,17 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): def _apply_hard_bounds(self, element, ranges): """ - Apply hard bounds to the x and y ranges of the plot. + Apply hard bounds to the x and y ranges of the plot. If xlim/ylim is set, limit the + initial viewable range to xlim/ylim, but allow navigation up to the abs max between + the data + pad range and xlim/ylim. If dim range is set (e.g. via redim.range), use + it as the hard bounds. - Sets the navigable bounds of the plot based on the extents - of the given element and ranges. If an extent is numeric and not NaN, it is - used as is. Otherwise, it is set to None, which means that end of the axis - is unbounded. """ def validate_bound(bound): - """Validate a single bound, returning None if it is not a valid number""" return bound if util.isfinite(bound) else None - min_extent_x, min_extent_y, max_extent_x, max_extent_y = map(validate_bound, self.get_extents(element, ranges, lims_as_soft_ranges=True)) + min_extent_x, min_extent_y, max_extent_x, max_extent_y = map(validate_bound, self.get_extents(element, ranges, range_type='combined', lims_as_soft_ranges=True)) def set_bounds(axis, min_extent, max_extent): """Set the bounds for a given axis, using None if both extents are None or identical""" diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 3cfc2517c1..d05f0c24db 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -1447,8 +1447,9 @@ def get_extents(self, element, ranges, range_type='combined', dimension=None, xd If lims_as_soft_ranges is set to True, the xlim and ylim will be treated as soft ranges instead of the default case as hard ranges while computing the extents. - This is useful when computing the maximum extents across data, padding, xlim/ylim, - and dimension ranges. + This is used e.g. when apply_hard_bounds is True and xlim/ylim is set, in which + case we limit the initial viewable range to xlim/ylim, but allow navigation up to + the abs max between the data + pad range and xlim/ylim. """ num = 6 if (isinstance(self.projection, str) and self.projection == '3d') else 4 if self.apply_extents and range_type in ('combined', 'extents'): From 6d252829782d9d8e8ea104d7e965483af8fdfda3 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Fri, 12 Apr 2024 13:22:02 -0700 Subject: [PATCH 11/24] run xylim through max_range to ensure datetime-dtype matching with ranges --- holoviews/plotting/bokeh/element.py | 7 ++++--- holoviews/plotting/plot.py | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index af1f996e2c..e0a8ec02b7 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -107,7 +107,8 @@ class ElementPlot(BokehPlot, GenericElementPlot): apply_hard_bounds = param.Boolean(default=False, doc=""" If True, the navigable bounds of the plot will be set based - on the larger of extents of the data+padding, xlim/ylim, dim ranges.""") + on the more extreme of extents between the data or xlim/ylim ranges. + If dim ranges are set, the hard bounds will be set to the dim ranges.""") autorange = param.ObjectSelector(default=None, objects=['x', 'y', None], doc=""" Whether to auto-range along either the x- or y-axis, i.e. @@ -1931,8 +1932,8 @@ def _apply_hard_bounds(self, element, ranges): """ Apply hard bounds to the x and y ranges of the plot. If xlim/ylim is set, limit the initial viewable range to xlim/ylim, but allow navigation up to the abs max between - the data + pad range and xlim/ylim. If dim range is set (e.g. via redim.range), use - it as the hard bounds. + the data range and xlim/ylim. If dim range is set (e.g. via redim.range), enforce + as hard bounds. """ diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index d05f0c24db..bcaedd2a50 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -1449,7 +1449,7 @@ def get_extents(self, element, ranges, range_type='combined', dimension=None, xd soft ranges instead of the default case as hard ranges while computing the extents. This is used e.g. when apply_hard_bounds is True and xlim/ylim is set, in which case we limit the initial viewable range to xlim/ylim, but allow navigation up to - the abs max between the data + pad range and xlim/ylim. + the abs max between the data range and xlim/ylim. """ num = 6 if (isinstance(self.projection, str) and self.projection == '3d') else 4 if self.apply_extents and range_type in ('combined', 'extents'): @@ -1493,8 +1493,11 @@ def get_extents(self, element, ranges, range_type='combined', dimension=None, xd x0, y0, x1, y1 = combined if lims_as_soft_ranges: - x0, x1 = util.dimension_range(x0, x1, (None, None), self.xlim) - y0, y1 = util.dimension_range(y0, y1, (None, None), self.ylim) + # run x|ylim through max_range to ensure datetime-dtype matching with ranges + xlim_soft_ranges = util.max_range([self.xlim]) + ylim_soft_ranges = util.max_range([self.ylim]) + x0, x1 = util.dimension_range(x0, x1, (None, None), xlim_soft_ranges) + y0, y1 = util.dimension_range(y0, y1, (None, None), ylim_soft_ranges) else: x0, x1 = util.dimension_range(x0, x1, self.xlim, (None, None)) y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) From b2d91105035f7328f45507508755afb4dc958282 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Fri, 12 Apr 2024 16:00:37 -0700 Subject: [PATCH 12/24] add tests --- .../tests/plotting/bokeh/test_elementplot.py | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py index ab6e210382..d5b5d29ce6 100644 --- a/holoviews/tests/plotting/bokeh/test_elementplot.py +++ b/holoviews/tests/plotting/bokeh/test_elementplot.py @@ -20,7 +20,7 @@ from holoviews.core.util import dt_to_int from holoviews.element import Curve, HeatMap, Image, Labels, Scatter from holoviews.plotting.util import process_cmap -from holoviews.streams import PointDraw, Stream +from holoviews.streams import PointDraw, Stream, param from holoviews.util import render from ...utils import LoggingComparisonTestCase @@ -993,3 +993,78 @@ def test_clim_percentile(self): low, high = plot.ranges[('Image',)]['z']['robust'] assert low > 0 assert high < 1 + +class TestApplyHardBounds(LoggingComparisonTestCase, TestBokehPlot): + + def test_apply_hard_bounds_with_xlim(self): + """Test `apply_hard_bounds` with `xlim` set. Initial view should be within xlim but allow panning to data range.""" + x_values = np.linspace(10, 50, 5) + y_values = np.array([10, 20, 30, 40, 50]) + curve = Curve((x_values, y_values)).opts(apply_hard_bounds=True, xlim=(15, 35)) + plot = self.bokeh_renderer.get_plot(curve) + initial_view_range = (plot.handles['x_range'].start, plot.handles['x_range'].end) + self.assertEqual(initial_view_range, (15, 35)) + # Check if data beyond xlim can be navigated to + self.assertEqual(plot.handles['x_range'].bounds, (10, 50)) + + def test_apply_hard_bounds_with_redim_range(self): + """Test `apply_hard_bounds` with `.redim.range(x=...)`. Hard bounds should strictly apply.""" + x_values = np.linspace(10, 50, 5) + y_values = np.array([10, 20, 30, 40, 50]) + curve = Curve((x_values, y_values)).redim.range(x=(25, None)).opts(apply_hard_bounds=True) + plot = self.bokeh_renderer.get_plot(curve) + # Expected to strictly adhere to any redim.range bounds, otherwise the data range + self.assertEqual((plot.handles['x_range'].start, plot.handles['x_range'].end), (25, 50)) + self.assertEqual(plot.handles['x_range'].bounds, (25, 50)) + + def test_apply_hard_bounds_datetime(self): + """Test datetime axes with hard bounds.""" + target_xlim_l = dt.datetime(2020, 1, 3) + target_xlim_h = dt.datetime(2020, 1, 7) + dates = [dt.datetime(2020, 1, i) for i in range(1, 11)] + values = np.linspace(0, 100, 10) + curve = Curve((dates, values)).opts( + apply_hard_bounds=True, + xlim=(target_xlim_l, target_xlim_h) + ) + plot = self.bokeh_renderer.get_plot(curve) + initial_view_range = (dt_to_int(plot.handles['x_range'].start), dt_to_int(plot.handles['x_range'].end)) + self.assertEqual(initial_view_range, (dt_to_int(target_xlim_l), dt_to_int(target_xlim_l))) + # Validate navigation bounds include entire data range + hard_bounds = (dt_to_int(plot.handles['x_range'].bounds[0]), dt_to_int(plot.handles['x_range'].bounds[1])) + self.assertEqual(hard_bounds, (dt_to_int(dt.datetime(2020, 1, 1)), dt_to_int(dt.datetime(2020, 1, 10)))) + + + def test_dynamic_map_bounds_update(self): + """Test that `apply_hard_bounds` applies correctly when DynamicMap is updated.""" + + def curve_data(choice): + datasets = { + 'set1': (np.linspace(0, 5, 100), np.random.rand(100)), + 'set2': (np.linspace(0, 20, 100), np.random.rand(100)), + } + x, y = datasets[choice] + return Curve((x, y)) + + ChoiceStream = Stream.define( + 'Choice', + choice=param.ObjectSelector(default='set1', objects=['set1', 'set2']) + ) + choice_stream = ChoiceStream() + dmap = DynamicMap(curve_data, kdims=[], streams=[choice_stream]) + dmap = dmap.opts(apply_hard_bounds=True, xlim=(2,3), framewise=True) + dmap = dmap.redim.values(choice=['set1', 'set2']) + plot = self.bokeh_renderer.get_plot(dmap) + + # Keeping the xlim consistent between updates, and change data range bounds + # Initially select 'set1' + dmap.event(choice='set1') + self.assertEqual(plot.handles['x_range'].start, 2) + self.assertEqual(plot.handles['x_range'].end, 3) + self.assertEqual(plot.handles['x_range'].bounds, (0, 5)) + + # Update to 'set2' + dmap.event(choice='set2') + self.assertEqual(plot.handles['x_range'].start, 2) + self.assertEqual(plot.handles['x_range'].end, 3) + self.assertEqual(plot.handles['x_range'].bounds, (0, 20)) From 48d15a5180fde8b8c90070b5b1572142a4e98da4 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Sun, 14 Apr 2024 18:22:57 -0700 Subject: [PATCH 13/24] apply hard bounds on every update --- holoviews/plotting/bokeh/element.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e0a8ec02b7..ab6487edb2 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -2077,6 +2077,9 @@ def update_frame(self, key, ranges=None, plot=None, element=None): cds = self.handles['cds'] self._postprocess_hover(renderer, cds) + if self.apply_hard_bounds: + self._apply_hard_bounds(element, ranges) + self._update_glyphs(element, ranges, self.style[self.cyclic_index]) self._execute_hooks(element) From 3133fe6da94b265b2f2da34006f35dacfe3ecea3 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 15 Apr 2024 10:29:00 -0700 Subject: [PATCH 14/24] update tests --- .../tests/plotting/bokeh/test_elementplot.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py index d5b5d29ce6..4ab2065634 100644 --- a/holoviews/tests/plotting/bokeh/test_elementplot.py +++ b/holoviews/tests/plotting/bokeh/test_elementplot.py @@ -16,6 +16,7 @@ tools, ) +from holoviews import opts from holoviews.core import DynamicMap, HoloMap, NdOverlay from holoviews.core.util import dt_to_int from holoviews.element import Curve, HeatMap, Image, Labels, Scatter @@ -994,14 +995,14 @@ def test_clim_percentile(self): assert low > 0 assert high < 1 -class TestApplyHardBounds(LoggingComparisonTestCase, TestBokehPlot): +class TestApplyHardBounds(TestBokehPlot): def test_apply_hard_bounds_with_xlim(self): """Test `apply_hard_bounds` with `xlim` set. Initial view should be within xlim but allow panning to data range.""" x_values = np.linspace(10, 50, 5) y_values = np.array([10, 20, 30, 40, 50]) curve = Curve((x_values, y_values)).opts(apply_hard_bounds=True, xlim=(15, 35)) - plot = self.bokeh_renderer.get_plot(curve) + plot = bokeh_renderer.get_plot(curve) initial_view_range = (plot.handles['x_range'].start, plot.handles['x_range'].end) self.assertEqual(initial_view_range, (15, 35)) # Check if data beyond xlim can be navigated to @@ -1012,7 +1013,7 @@ def test_apply_hard_bounds_with_redim_range(self): x_values = np.linspace(10, 50, 5) y_values = np.array([10, 20, 30, 40, 50]) curve = Curve((x_values, y_values)).redim.range(x=(25, None)).opts(apply_hard_bounds=True) - plot = self.bokeh_renderer.get_plot(curve) + plot = bokeh_renderer.get_plot(curve) # Expected to strictly adhere to any redim.range bounds, otherwise the data range self.assertEqual((plot.handles['x_range'].start, plot.handles['x_range'].end), (25, 50)) self.assertEqual(plot.handles['x_range'].bounds, (25, 50)) @@ -1027,9 +1028,9 @@ def test_apply_hard_bounds_datetime(self): apply_hard_bounds=True, xlim=(target_xlim_l, target_xlim_h) ) - plot = self.bokeh_renderer.get_plot(curve) + plot = bokeh_renderer.get_plot(curve) initial_view_range = (dt_to_int(plot.handles['x_range'].start), dt_to_int(plot.handles['x_range'].end)) - self.assertEqual(initial_view_range, (dt_to_int(target_xlim_l), dt_to_int(target_xlim_l))) + self.assertEqual(initial_view_range, (dt_to_int(target_xlim_l), dt_to_int(target_xlim_h))) # Validate navigation bounds include entire data range hard_bounds = (dt_to_int(plot.handles['x_range'].bounds[0]), dt_to_int(plot.handles['x_range'].bounds[1])) self.assertEqual(hard_bounds, (dt_to_int(dt.datetime(2020, 1, 1)), dt_to_int(dt.datetime(2020, 1, 10)))) @@ -1052,9 +1053,9 @@ def curve_data(choice): ) choice_stream = ChoiceStream() dmap = DynamicMap(curve_data, kdims=[], streams=[choice_stream]) - dmap = dmap.opts(apply_hard_bounds=True, xlim=(2,3), framewise=True) + dmap = dmap.opts(opts.Curve(apply_hard_bounds=True, xlim=(2,3), framewise=True)) dmap = dmap.redim.values(choice=['set1', 'set2']) - plot = self.bokeh_renderer.get_plot(dmap) + plot = bokeh_renderer.get_plot(dmap) # Keeping the xlim consistent between updates, and change data range bounds # Initially select 'set1' From ea187a815c9130358b92e2e950e007a72a6e0b70 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 15 Apr 2024 10:43:26 -0700 Subject: [PATCH 15/24] fix test callback --- holoviews/tests/ui/bokeh/test_callback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index e577424440..8c21db5fb0 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -127,7 +127,7 @@ def test_rangexy(serve_hv): wait_until(lambda: rangexy.x_range == expected_xrange and rangexy.y_range == expected_yrange, page) @pytest.mark.usefixtures("bokeh_backend") -def test_multi_axis_rangexy(page, port): +def test_multi_axis_rangexy(serve_hv): c1 = Curve(np.arange(100).cumsum(), vdims='y').opts(apply_hard_bounds=False) c2 = Curve(-np.arange(100).cumsum(), vdims='y2').opts(apply_hard_bounds=False) s1 = RangeXY(source=c1) From 45cc495881164553ea17b8aa48f3091a42fa58ca Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 15 Apr 2024 11:26:38 -0700 Subject: [PATCH 16/24] add overlay and single curve tests --- .../tests/plotting/bokeh/test_elementplot.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py index 4ab2065634..38ce8cfb25 100644 --- a/holoviews/tests/plotting/bokeh/test_elementplot.py +++ b/holoviews/tests/plotting/bokeh/test_elementplot.py @@ -17,7 +17,7 @@ ) from holoviews import opts -from holoviews.core import DynamicMap, HoloMap, NdOverlay +from holoviews.core import DynamicMap, HoloMap, NdOverlay, Overlay from holoviews.core.util import dt_to_int from holoviews.element import Curve, HeatMap, Image, Labels, Scatter from holoviews.plotting.util import process_cmap @@ -996,6 +996,25 @@ def test_clim_percentile(self): assert high < 1 class TestApplyHardBounds(TestBokehPlot): + def test_apply_hard_bounds(self): + """Test `apply_hard_bounds` with a single element.""" + x_values = np.linspace(10, 50, 5) + y_values = np.array([10, 20, 30, 40, 50]) + curve = Curve((x_values, y_values)).opts(apply_hard_bounds=True) + plot = bokeh_renderer.get_plot(curve) + self.assertEqual(plot.handles['x_range'].bounds, (10, 50)) + + def test_apply_hard_bounds_overlay(self): + """Test `apply_hard_bounds` with an overlay of curves.""" + x1_values = np.linspace(10, 50, 5) + x2_values = np.linspace(10, 90, 5) + y_values = np.array([10, 20, 30, 40, 50]) + curve1 = Curve((x1_values, y_values)) + curve2 = Curve((x2_values, y_values)) + overlay = Overlay([curve1, curve2]).opts(opts.Curve(apply_hard_bounds=True)) + plot = bokeh_renderer.get_plot(overlay) + # Check if the large of the data range can be navigated to + self.assertEqual(plot.handles['x_range'].bounds, (10, 90)) def test_apply_hard_bounds_with_xlim(self): """Test `apply_hard_bounds` with `xlim` set. Initial view should be within xlim but allow panning to data range.""" @@ -1035,7 +1054,6 @@ def test_apply_hard_bounds_datetime(self): hard_bounds = (dt_to_int(plot.handles['x_range'].bounds[0]), dt_to_int(plot.handles['x_range'].bounds[1])) self.assertEqual(hard_bounds, (dt_to_int(dt.datetime(2020, 1, 1)), dt_to_int(dt.datetime(2020, 1, 10)))) - def test_dynamic_map_bounds_update(self): """Test that `apply_hard_bounds` applies correctly when DynamicMap is updated.""" From 4056f6e10ad9551bd6e292c8f6841e4cf5e1767a Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 15 Apr 2024 12:17:24 -0700 Subject: [PATCH 17/24] add docs --- examples/user_guide/Plotting_with_Bokeh.ipynb | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/examples/user_guide/Plotting_with_Bokeh.ipynb b/examples/user_guide/Plotting_with_Bokeh.ipynb index 1e7fa587b6..5c78d99671 100644 --- a/examples/user_guide/Plotting_with_Bokeh.ipynb +++ b/examples/user_guide/Plotting_with_Bokeh.ipynb @@ -296,6 +296,78 @@ " img.options(data_aspect=2, frame_width=300).relabel('data_aspect=2')).cols(2)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Navigable Bounds\n", + "\n", + "Users may set the `apply_hard_bounds` option to constrain the navigable range (extent one could zoom or pan to). If `True`, the navigable bounds of the plot will be constrained to the range of the data. Go ahead and try to zoom in a out in the plot below, you should find that you cannot zoom beyond the extents of the data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_values = np.linspace(0, 10, 100)\n", + "y_values = np.sin(x_values)\n", + "\n", + "hv.Curve((x_values, y_values)).opts(apply_hard_bounds=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If `xlim` or `ylim` is set for an element, the navigable bounds of the plot will be set based\n", + "on the combined extremes of extents between the data and xlim/ylim ranges. In the plot below, the `xlim` constrains the initial view, but you should be able to pan to the x-range between 0 and 12 - the combined extremes of ranges between the data (0,10) and `xlim` (2,12)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_values = np.linspace(0, 10, 100)\n", + "y_values = np.sin(x_values)\n", + "\n", + "hv.Curve((x_values, y_values)).opts(\n", + " apply_hard_bounds=True,\n", + " xlim=(2, 12),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If a dimension range is specified (e.g. with `.redim.range`), this range will be used as the hard bounds, regardless of the data range or xlim/ylim. This is because the dimension range is itended to be an override on the minimum and maximum allowable values for the dimension. Read more in [Annotating your Data](./01-Annotating_Data.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_values = np.linspace(0, 10, 100)\n", + "y_values = np.sin(x_values)\n", + "\n", + "hv.Curve((x_values, y_values)).opts(\n", + " apply_hard_bounds=True,\n", + ").redim.range(x=(4, 6))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the plot above, you should not be able to navigate beyond the specified dimension ranges of `x` (4, 6). " + ] + }, { "cell_type": "markdown", "metadata": {}, From 05b9b0f98eb7081628ef305a2907a659ab5a9ef8 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 16 Apr 2024 09:45:00 -0700 Subject: [PATCH 18/24] remove apply hard bounds from test_callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Simon Høxbro Hansen --- holoviews/tests/ui/bokeh/test_callback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 8c21db5fb0..f08e056744 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -128,8 +128,8 @@ def test_rangexy(serve_hv): @pytest.mark.usefixtures("bokeh_backend") def test_multi_axis_rangexy(serve_hv): - c1 = Curve(np.arange(100).cumsum(), vdims='y').opts(apply_hard_bounds=False) - c2 = Curve(-np.arange(100).cumsum(), vdims='y2').opts(apply_hard_bounds=False) + c1 = Curve(np.arange(100).cumsum(), vdims='y') + c2 = Curve(-np.arange(100).cumsum(), vdims='y2') s1 = RangeXY(source=c1) s2 = RangeXY(source=c2) From 38b396b511d02bfc7a9b9736fcf11796bf06e5c9 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 16 Apr 2024 09:45:56 -0700 Subject: [PATCH 19/24] wrap long line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Simon Høxbro Hansen --- holoviews/plotting/bokeh/element.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index ab6487edb2..6343cbf0bc 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1940,7 +1940,9 @@ def _apply_hard_bounds(self, element, ranges): def validate_bound(bound): return bound if util.isfinite(bound) else None - min_extent_x, min_extent_y, max_extent_x, max_extent_y = map(validate_bound, self.get_extents(element, ranges, range_type='combined', lims_as_soft_ranges=True)) + min_extent_x, min_extent_y, max_extent_x, max_extent_y = map( + validate_bound, self.get_extents(element, ranges, range_type='combined', lims_as_soft_ranges=True) + ) def set_bounds(axis, min_extent, max_extent): """Set the bounds for a given axis, using None if both extents are None or identical""" From fad2beeaceb92427d70a13961ea68aa827560ba3 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 16 Apr 2024 10:01:02 -0700 Subject: [PATCH 20/24] remove unnecessary conditionals --- holoviews/plotting/bokeh/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 6343cbf0bc..e0abbe75c2 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1949,7 +1949,7 @@ def set_bounds(axis, min_extent, max_extent): if min_extent == max_extent: self.handles[axis].bounds = None else: - self.handles[axis].bounds = (min_extent, max_extent) if min_extent is not None or max_extent is not None else None + self.handles[axis].bounds = (min_extent, max_extent) set_bounds('x_range', min_extent_x, max_extent_x) set_bounds('y_range', min_extent_y, max_extent_y) From bae1705fd73d60a232fc06981f02aa172b01479d Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 16 Apr 2024 10:50:43 -0700 Subject: [PATCH 21/24] use assert for tests --- .../tests/plotting/bokeh/test_elementplot.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py index 38ce8cfb25..f08545b899 100644 --- a/holoviews/tests/plotting/bokeh/test_elementplot.py +++ b/holoviews/tests/plotting/bokeh/test_elementplot.py @@ -1002,7 +1002,7 @@ def test_apply_hard_bounds(self): y_values = np.array([10, 20, 30, 40, 50]) curve = Curve((x_values, y_values)).opts(apply_hard_bounds=True) plot = bokeh_renderer.get_plot(curve) - self.assertEqual(plot.handles['x_range'].bounds, (10, 50)) + assert plot.handles['x_range'].bounds == (10, 50) def test_apply_hard_bounds_overlay(self): """Test `apply_hard_bounds` with an overlay of curves.""" @@ -1014,7 +1014,7 @@ def test_apply_hard_bounds_overlay(self): overlay = Overlay([curve1, curve2]).opts(opts.Curve(apply_hard_bounds=True)) plot = bokeh_renderer.get_plot(overlay) # Check if the large of the data range can be navigated to - self.assertEqual(plot.handles['x_range'].bounds, (10, 90)) + assert plot.handles['x_range'].bounds == (10, 90) def test_apply_hard_bounds_with_xlim(self): """Test `apply_hard_bounds` with `xlim` set. Initial view should be within xlim but allow panning to data range.""" @@ -1023,9 +1023,9 @@ def test_apply_hard_bounds_with_xlim(self): curve = Curve((x_values, y_values)).opts(apply_hard_bounds=True, xlim=(15, 35)) plot = bokeh_renderer.get_plot(curve) initial_view_range = (plot.handles['x_range'].start, plot.handles['x_range'].end) - self.assertEqual(initial_view_range, (15, 35)) + assert initial_view_range == (15, 35) # Check if data beyond xlim can be navigated to - self.assertEqual(plot.handles['x_range'].bounds, (10, 50)) + assert plot.handles['x_range'].bounds == (10, 50) def test_apply_hard_bounds_with_redim_range(self): """Test `apply_hard_bounds` with `.redim.range(x=...)`. Hard bounds should strictly apply.""" @@ -1034,8 +1034,8 @@ def test_apply_hard_bounds_with_redim_range(self): curve = Curve((x_values, y_values)).redim.range(x=(25, None)).opts(apply_hard_bounds=True) plot = bokeh_renderer.get_plot(curve) # Expected to strictly adhere to any redim.range bounds, otherwise the data range - self.assertEqual((plot.handles['x_range'].start, plot.handles['x_range'].end), (25, 50)) - self.assertEqual(plot.handles['x_range'].bounds, (25, 50)) + assert (plot.handles['x_range'].start, plot.handles['x_range'].end) == (25, 50) + assert plot.handles['x_range'].bounds == (25, 50) def test_apply_hard_bounds_datetime(self): """Test datetime axes with hard bounds.""" @@ -1049,10 +1049,10 @@ def test_apply_hard_bounds_datetime(self): ) plot = bokeh_renderer.get_plot(curve) initial_view_range = (dt_to_int(plot.handles['x_range'].start), dt_to_int(plot.handles['x_range'].end)) - self.assertEqual(initial_view_range, (dt_to_int(target_xlim_l), dt_to_int(target_xlim_h))) + assert initial_view_range == (dt_to_int(target_xlim_l), dt_to_int(target_xlim_h)) # Validate navigation bounds include entire data range hard_bounds = (dt_to_int(plot.handles['x_range'].bounds[0]), dt_to_int(plot.handles['x_range'].bounds[1])) - self.assertEqual(hard_bounds, (dt_to_int(dt.datetime(2020, 1, 1)), dt_to_int(dt.datetime(2020, 1, 10)))) + assert hard_bounds == (dt_to_int(dt.datetime(2020, 1, 1)), dt_to_int(dt.datetime(2020, 1, 10))) def test_dynamic_map_bounds_update(self): """Test that `apply_hard_bounds` applies correctly when DynamicMap is updated.""" @@ -1078,12 +1078,12 @@ def curve_data(choice): # Keeping the xlim consistent between updates, and change data range bounds # Initially select 'set1' dmap.event(choice='set1') - self.assertEqual(plot.handles['x_range'].start, 2) - self.assertEqual(plot.handles['x_range'].end, 3) - self.assertEqual(plot.handles['x_range'].bounds, (0, 5)) + assert plot.handles['x_range'].start == 2 + assert plot.handles['x_range'].end == 3 + assert plot.handles['x_range'].bounds == (0, 5) # Update to 'set2' dmap.event(choice='set2') - self.assertEqual(plot.handles['x_range'].start, 2) - self.assertEqual(plot.handles['x_range'].end, 3) - self.assertEqual(plot.handles['x_range'].bounds, (0, 20)) + assert plot.handles['x_range'].start == 2 + assert plot.handles['x_range'].end == 3 + assert plot.handles['x_range'].bounds == (0, 20) From 4432966c4e68bf585b974716181b6c0547c30bf6 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 16 Apr 2024 11:17:37 -0700 Subject: [PATCH 22/24] one liner set bounds --- holoviews/plotting/bokeh/element.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e0abbe75c2..3ba7fa98ef 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1946,10 +1946,7 @@ def validate_bound(bound): def set_bounds(axis, min_extent, max_extent): """Set the bounds for a given axis, using None if both extents are None or identical""" - if min_extent == max_extent: - self.handles[axis].bounds = None - else: - self.handles[axis].bounds = (min_extent, max_extent) + self.handles[axis].bounds = None if min_extent == max_extent else (min_extent, max_extent) set_bounds('x_range', min_extent_x, max_extent_x) set_bounds('y_range', min_extent_y, max_extent_y) From 828ec3d0e2df6b32df8501fd40ba9147d803f0ae Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 16 Apr 2024 11:18:54 -0700 Subject: [PATCH 23/24] import param directly --- holoviews/tests/plotting/bokeh/test_elementplot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py index f08545b899..9cef2f3b6c 100644 --- a/holoviews/tests/plotting/bokeh/test_elementplot.py +++ b/holoviews/tests/plotting/bokeh/test_elementplot.py @@ -3,6 +3,7 @@ import numpy as np import panel as pn +import param import pytest from bokeh.document import Document from bokeh.models import ( @@ -21,7 +22,7 @@ from holoviews.core.util import dt_to_int from holoviews.element import Curve, HeatMap, Image, Labels, Scatter from holoviews.plotting.util import process_cmap -from holoviews.streams import PointDraw, Stream, param +from holoviews.streams import PointDraw, Stream from holoviews.util import render from ...utils import LoggingComparisonTestCase From ca106d1a4387a67df4440990d678842fdb900a3f Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 16 Apr 2024 11:32:13 -0700 Subject: [PATCH 24/24] add catch for categorical FactorRange axes --- holoviews/plotting/bokeh/element.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 3ba7fa98ef..e49695a0fa 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1946,7 +1946,10 @@ def validate_bound(bound): def set_bounds(axis, min_extent, max_extent): """Set the bounds for a given axis, using None if both extents are None or identical""" - self.handles[axis].bounds = None if min_extent == max_extent else (min_extent, max_extent) + try: + self.handles[axis].bounds = None if min_extent == max_extent else (min_extent, max_extent) + except ValueError: + self.handles[axis].bounds = None set_bounds('x_range', min_extent_x, max_extent_x) set_bounds('y_range', min_extent_y, max_extent_y)