From 49f3f41e43b4880a4f2d67cd42129a9776fa5a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Sat, 23 Sep 2023 10:38:41 +0200 Subject: [PATCH 1/8] Don't overwrite axis labels if overlaid --- holoviews/plotting/bokeh/annotation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index 201d397d24..c33abdc729 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -51,6 +51,8 @@ def get_data(self, element, ranges, style): def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): figure = super().initialize_plot(ranges=ranges, plot=plot, plots=plots, source=source) + if self.overlaid: + return figure labels = "yx" if self.invert_axes else "xy" for ax, label in zip(figure.axis, labels): ax.axis_label = label From a9821466e56aa912d2db1af02dd78df4ddd8f51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Sat, 23 Sep 2023 10:39:39 +0200 Subject: [PATCH 2/8] Handle when extents is None --- holoviews/plotting/bokeh/annotation.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index c33abdc729..5971ad63c9 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -3,6 +3,7 @@ import param import numpy as np +import pandas as pd from bokeh.models import BoxAnnotation, Span, Arrow, Slope from panel.models import HTML @@ -65,9 +66,11 @@ def get_extents(self, element, ranges=None, range_type='combined', **kwargs): elif isinstance(element, VLines): extents = extents[0], np.nan, extents[2], np.nan elif isinstance(element, HSpans): - extents = np.nan, min(extents[:2]), np.nan, max(extents[2:]) + extents = pd.array(extents) # Handle both nan and None + extents = np.nan, extents[:2].min(), np.nan, extents[2:].max() elif isinstance(element, VSpans): - extents = min(extents[:2]), np.nan, max(extents[2:]), np.nan + extents = pd.array(extents) # Handle both nan and None + extents = extents[:2].min(), np.nan, extents[2:].max(), np.nan return extents class HLinesAnnotationPlot(_SyntheticAnnotationPlot): From 7b0d8bb600fb4b893f1d0d4911ed7fca3267caa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Sat, 23 Sep 2023 10:40:00 +0200 Subject: [PATCH 3/8] Handle Pandas 2.1 series iloc warning --- holoviews/plotting/bokeh/element.py | 2 +- holoviews/plotting/bokeh/plot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 079311b93e..ac9b4bde6b 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1681,7 +1681,7 @@ def _postprocess_hover(self, renderer, source): for k, values in source.data.items(): key = '@{%s}' % k if ((isinstance(value, np.ndarray) and value.dtype.kind == 'M') or - (len(values) and isinstance(values[0], util.datetime_types))): + (len(values) and isinstance(np.asarray(values)[0], util.datetime_types))): hover.tooltips = [(l, f+'{%F %T}' if f == key else f) for l, f in hover.tooltips] hover.formatters[key] = "datetime" diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 5ed81c0d86..c995ad3629 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -131,7 +131,7 @@ def _postprocess_data(self, data): values = decode_bytes(values) # Bytes need decoding to strings # Certain datetime types need to be converted - if len(values) and isinstance(values[0], cftime_types): + if len(values) and isinstance(np.asarray(values)[0], cftime_types): if any(v.calendar not in _STANDARD_CALENDARS for v in values): self.param.warning( 'Converting cftime.datetime from a non-standard ' From a457b91953cd9cc5796f75e46707b7fd567c836b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 28 Sep 2023 19:03:14 +0200 Subject: [PATCH 4/8] Use next(iter(...)) instead of np.asarray(...)[0] --- holoviews/plotting/bokeh/element.py | 2 +- holoviews/plotting/bokeh/plot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 5b761569c9..51696e24b8 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1767,7 +1767,7 @@ def _postprocess_hover(self, renderer, source): for k, values in source.data.items(): key = '@{%s}' % k if ((isinstance(value, np.ndarray) and value.dtype.kind == 'M') or - (len(values) and isinstance(np.asarray(values)[0], util.datetime_types))): + (len(values) and isinstance(next(iter(values)), util.datetime_types))): hover.tooltips = [(l, f+'{%F %T}' if f == key else f) for l, f in hover.tooltips] hover.formatters[key] = "datetime" diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index c995ad3629..d6cb0c18f3 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -131,7 +131,7 @@ def _postprocess_data(self, data): values = decode_bytes(values) # Bytes need decoding to strings # Certain datetime types need to be converted - if len(values) and isinstance(np.asarray(values)[0], cftime_types): + if len(values) and isinstance(next(iter(values)), cftime_types): if any(v.calendar not in _STANDARD_CALENDARS for v in values): self.param.warning( 'Converting cftime.datetime from a non-standard ' From b585a03f8f0d46e7516990a505b4d2f56b95f1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 28 Sep 2023 19:05:48 +0200 Subject: [PATCH 5/8] Handle when extents is None in mpl --- holoviews/plotting/mpl/annotation.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/mpl/annotation.py b/holoviews/plotting/mpl/annotation.py index 1466c56b1e..5a88c3ce12 100644 --- a/holoviews/plotting/mpl/annotation.py +++ b/holoviews/plotting/mpl/annotation.py @@ -1,6 +1,7 @@ import param import numpy as np import matplotlib as mpl +import pandas as pd from matplotlib import patches from matplotlib.lines import Line2D @@ -312,9 +313,11 @@ def get_extents(self, element, ranges=None, range_type='combined', **kwargs): elif isinstance(element, VLines): extents = extents[0], np.nan, extents[2], np.nan elif isinstance(element, HSpans): - extents = np.nan, min(extents[:2]), np.nan, max(extents[2:]) + extents = pd.array(extents) # Handle both nan and None + extents = np.nan, extents[:2].min(), np.nan, extents[2:].max() elif isinstance(element, VSpans): - extents = min(extents[:2]), np.nan, max(extents[2:]), np.nan + extents = pd.array(extents) # Handle both nan and None + extents = extents[:2].min(), np.nan, extents[2:].max(), np.nan return extents def initialize_plot(self, ranges=None): From e3ae97689b171f6522220c4a9d5bdf0ba7198ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 29 Sep 2023 11:41:00 +0200 Subject: [PATCH 6/8] Only force labels if type no other Element are used --- holoviews/plotting/bokeh/annotation.py | 4 +++- .../plotting/bokeh/test_annotationplot.py | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index 5971ad63c9..70e8e0d421 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -1,3 +1,4 @@ +import itertools from collections import defaultdict from html import escape @@ -52,7 +53,8 @@ def get_data(self, element, ranges, style): def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): figure = super().initialize_plot(ranges=ranges, plot=plot, plots=plots, source=source) - if self.overlaid: + # Only force labels if no other ranges are set + if self.overlaid and set(itertools.chain.from_iterable(ranges)) - {"HSpans", "VSpans", "VLines", "HLines"}: return figure labels = "yx" if self.invert_axes else "xy" for ax, label in zip(figure.axis, labels): diff --git a/holoviews/tests/plotting/bokeh/test_annotationplot.py b/holoviews/tests/plotting/bokeh/test_annotationplot.py index b9da8a1139..ac8e1861f9 100644 --- a/holoviews/tests/plotting/bokeh/test_annotationplot.py +++ b/holoviews/tests/plotting/bokeh/test_annotationplot.py @@ -357,6 +357,18 @@ def test_vlines_hlines_overlay(self): assert plot.handles["y_range"].start == 0 assert plot.handles["y_range"].end == 5.5 + def test_vlines_hlines_overlay_non_annotation(self): + non_annotation = hv.Curve([], kdims=["time"]) + hlines = HLines( + {"y": [0, 1, 2, 5.5], "extra": [-1, -2, -3, -44]}, vdims=["extra"] + ) + vlines = VLines( + {"x": [0, 1, 2, 5.5], "extra": [-1, -2, -3, -44]}, vdims=["extra"] + ) + plot = bokeh_renderer.get_plot(non_annotation * hlines * vlines) + assert plot.handles["xaxis"].axis_label == "time" + assert plot.handles["yaxis"].axis_label == "y" + def test_coloring_hline(self): hlines = HLines({"y": [1, 2, 3]}) hlines = hlines.opts( @@ -514,6 +526,18 @@ def test_vspans_hspans_overlay(self): assert plot.handles["y_range"].start == 0 assert plot.handles["y_range"].end == 6.5 + def test_vlines_hlines_overlay_non_annotation(self): + non_annotation = hv.Curve([], kdims=["time"]) + hspans = HSpans( + {"y0": [0, 3, 5.5], "y1": [1, 4, 6.5], "extra": [-1, -2, -3]}, vdims=["extra"] + ) + vspans = VSpans( + {"x0": [0, 3, 5.5], "x1": [1, 4, 6.5], "extra": [-1, -2, -3]}, vdims=["extra"] + ) + plot = bokeh_renderer.get_plot(non_annotation * hspans * vspans) + assert plot.handles["xaxis"].axis_label == "time" + assert plot.handles["yaxis"].axis_label == "y" + def test_coloring_hline(self): hspans = HSpans({"y0": [1, 3, 5], "y1": [2, 4, 6]}).opts( alpha=hv.dim("y0").norm(), From 164949e9664c01b74ee4946f19a504d3d82e9084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 29 Sep 2023 12:09:52 +0200 Subject: [PATCH 7/8] Add custom labels --- holoviews/plotting/bokeh/annotation.py | 3 ++- .../plotting/bokeh/test_annotationplot.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index 70e8e0d421..6c77964564 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -56,7 +56,8 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): # Only force labels if no other ranges are set if self.overlaid and set(itertools.chain.from_iterable(ranges)) - {"HSpans", "VSpans", "VLines", "HLines"}: return figure - labels = "yx" if self.invert_axes else "xy" + labels = [self.xlabel or "x", self.ylabel or "y"] + labels = labels[::-1] if self.invert_axes else labels for ax, label in zip(figure.axis, labels): ax.axis_label = label return figure diff --git a/holoviews/tests/plotting/bokeh/test_annotationplot.py b/holoviews/tests/plotting/bokeh/test_annotationplot.py index ac8e1861f9..3525e80a1d 100644 --- a/holoviews/tests/plotting/bokeh/test_annotationplot.py +++ b/holoviews/tests/plotting/bokeh/test_annotationplot.py @@ -232,6 +232,15 @@ def test_hlines_plot(self): assert (source.data["y"] == [0, 1, 2, 5.5]).all() assert (source.data["extra"] == [-1, -2, -3, -44]).all() + def test_hlines_xlabel_ylabel(self): + hlines = HLines( + {"y": [0, 1, 2, 5.5], "extra": [-1, -2, -3, -44]}, vdims=["extra"] + ).opts(xlabel="xlabel", ylabel="xlabel") + plot = bokeh_renderer.get_plot(hlines) + assert isinstance(plot.handles["glyph"], BkHSpan) + assert plot.handles["xaxis"].axis_label == "xlabel" + assert plot.handles["yaxis"].axis_label == "xlabel" + def test_hlines_array(self): hlines = HLines(np.array([0, 1, 2, 5.5])) plot = bokeh_renderer.get_plot(hlines) @@ -412,6 +421,15 @@ def test_hspans_plot(self): assert (source.data["y1"] == [1, 4, 6.5]).all() assert (source.data["extra"] == [-1, -2, -3]).all() + def test_hspans_plot_xlabel_ylabel(self): + hspans = HSpans( + {"y0": [0, 3, 5.5], "y1": [1, 4, 6.5], "extra": [-1, -2, -3]}, vdims=["extra"] + ).opts(xlabel="xlabel", ylabel="xlabel") + plot = bokeh_renderer.get_plot(hspans) + assert isinstance(plot.handles["glyph"], BkHStrip) + assert plot.handles["xaxis"].axis_label == "xlabel" + assert plot.handles["yaxis"].axis_label == "xlabel" + def test_hspans_plot_invert_axes(self): hspans = HSpans( {"y0": [0, 3, 5.5], "y1": [1, 4, 6.5], "extra": [-1, -2, -3]}, vdims=["extra"] From 6b506b7dbc23579b977404c4a311070f3a3c537a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 29 Sep 2023 12:50:06 +0200 Subject: [PATCH 8/8] Reverts changes I can't recreate --- holoviews/plotting/bokeh/annotation.py | 7 ++----- holoviews/plotting/bokeh/element.py | 2 +- holoviews/plotting/bokeh/plot.py | 2 +- holoviews/plotting/mpl/annotation.py | 7 ++----- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index 6c77964564..1bef23a4c1 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -4,7 +4,6 @@ import param import numpy as np -import pandas as pd from bokeh.models import BoxAnnotation, Span, Arrow, Slope from panel.models import HTML @@ -69,11 +68,9 @@ def get_extents(self, element, ranges=None, range_type='combined', **kwargs): elif isinstance(element, VLines): extents = extents[0], np.nan, extents[2], np.nan elif isinstance(element, HSpans): - extents = pd.array(extents) # Handle both nan and None - extents = np.nan, extents[:2].min(), np.nan, extents[2:].max() + extents = np.nan, min(extents[:2]), np.nan, max(extents[2:]) elif isinstance(element, VSpans): - extents = pd.array(extents) # Handle both nan and None - extents = extents[:2].min(), np.nan, extents[2:].max(), np.nan + extents = min(extents[:2]), np.nan, max(extents[2:]), np.nan return extents class HLinesAnnotationPlot(_SyntheticAnnotationPlot): diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 51696e24b8..c1d20faa20 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1767,7 +1767,7 @@ def _postprocess_hover(self, renderer, source): for k, values in source.data.items(): key = '@{%s}' % k if ((isinstance(value, np.ndarray) and value.dtype.kind == 'M') or - (len(values) and isinstance(next(iter(values)), util.datetime_types))): + (len(values) and isinstance(values[0], util.datetime_types))): hover.tooltips = [(l, f+'{%F %T}' if f == key else f) for l, f in hover.tooltips] hover.formatters[key] = "datetime" diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index d6cb0c18f3..5ed81c0d86 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -131,7 +131,7 @@ def _postprocess_data(self, data): values = decode_bytes(values) # Bytes need decoding to strings # Certain datetime types need to be converted - if len(values) and isinstance(next(iter(values)), cftime_types): + if len(values) and isinstance(values[0], cftime_types): if any(v.calendar not in _STANDARD_CALENDARS for v in values): self.param.warning( 'Converting cftime.datetime from a non-standard ' diff --git a/holoviews/plotting/mpl/annotation.py b/holoviews/plotting/mpl/annotation.py index 5a88c3ce12..1466c56b1e 100644 --- a/holoviews/plotting/mpl/annotation.py +++ b/holoviews/plotting/mpl/annotation.py @@ -1,7 +1,6 @@ import param import numpy as np import matplotlib as mpl -import pandas as pd from matplotlib import patches from matplotlib.lines import Line2D @@ -313,11 +312,9 @@ def get_extents(self, element, ranges=None, range_type='combined', **kwargs): elif isinstance(element, VLines): extents = extents[0], np.nan, extents[2], np.nan elif isinstance(element, HSpans): - extents = pd.array(extents) # Handle both nan and None - extents = np.nan, extents[:2].min(), np.nan, extents[2:].max() + extents = np.nan, min(extents[:2]), np.nan, max(extents[2:]) elif isinstance(element, VSpans): - extents = pd.array(extents) # Handle both nan and None - extents = extents[:2].min(), np.nan, extents[2:].max(), np.nan + extents = min(extents[:2]), np.nan, max(extents[2:]), np.nan return extents def initialize_plot(self, ranges=None):