From eb79ab52414f53b5990f2b624edd4ba582a3fa6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 4 Jul 2024 11:34:25 +0200 Subject: [PATCH] comp(matplotlib): Add compatibility with Matplotlib 3.9 (#6307) --- holoviews/plotting/mpl/chart.py | 15 +++-- holoviews/plotting/mpl/stats.py | 18 ++++-- holoviews/plotting/mpl/util.py | 2 + .../matplotlib/test_annotationplot.py | 60 +++++++++++++------ .../plotting/matplotlib/test_boxwhisker.py | 6 +- .../plotting/matplotlib/test_violinplot.py | 11 +++- pyproject.toml | 2 + 7 files changed, 84 insertions(+), 30 deletions(-) diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 879fd260e6..5da620b859 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -1,7 +1,6 @@ import matplotlib as mpl import numpy as np import param -from matplotlib import cm from matplotlib.collections import LineCollection from matplotlib.dates import DateFormatter, date2num from packaging.version import Version @@ -27,7 +26,7 @@ from .element import ColorbarPlot, ElementPlot, LegendPlot from .path import PathPlot from .plot import AdjoinedPlot, mpl_rc_context -from .util import mpl_version +from .util import MPL_GE_3_7, MPL_GE_3_9, mpl_version class ChartPlot(ElementPlot): @@ -95,7 +94,10 @@ def get_data(self, element, ranges, style): def init_artists(self, ax, plot_args, plot_kwargs): xs, ys = plot_args if isdatetime(xs): - artist = ax.plot_date(xs, ys, '-', **plot_kwargs)[0] + if MPL_GE_3_9: + artist = ax.plot(xs, ys, '-', **plot_kwargs)[0] + else: + artist = ax.plot_date(xs, ys, '-', **plot_kwargs)[0] else: artist = ax.plot(xs, ys, **plot_kwargs)[0] return {'artist': artist} @@ -501,7 +503,12 @@ def _update_plot(self, key, element, bars, lims, ranges): # Get colormapping options if isinstance(range_item, (HeatMap, Raster)) or (cdim and cdim in element): style = self.lookup_options(range_item, 'style')[self.cyclic_index] - cmap = cm.get_cmap(style.get('cmap')) + if MPL_GE_3_7: + # https://github.com/matplotlib/matplotlib/pull/28355 + cmap = mpl.colormaps.get_cmap(style.get('cmap')) + else: + from matplotlib import cm + cmap = cm.get_cmap(style.get('cmap')) main_range = style.get('clims', main_range) else: cmap = None diff --git a/holoviews/plotting/mpl/stats.py b/holoviews/plotting/mpl/stats.py index f728404c3b..9764de4e11 100644 --- a/holoviews/plotting/mpl/stats.py +++ b/holoviews/plotting/mpl/stats.py @@ -6,6 +6,7 @@ from .chart import AreaPlot, ChartPlot from .path import PolygonPlot from .plot import AdjoinedPlot +from .util import MPL_GE_3_9 class DistributionPlot(AreaPlot): @@ -77,7 +78,10 @@ def get_data(self, element, ranges, style): d = group[group.vdims[0]] data.append(d[np.isfinite(d)]) labels.append(label) - style['labels'] = labels + if MPL_GE_3_9: + style['tick_labels'] = labels + else: + style['labels'] = labels style = {k: v for k, v in style.items() if k not in ['zorder', 'label']} style['vert'] = not self.invert_axes @@ -157,7 +161,10 @@ def init_artists(self, ax, plot_args, plot_kwargs): stats_color = plot_kwargs.pop('stats_color', 'black') facecolors = plot_kwargs.pop('facecolors', []) edgecolors = plot_kwargs.pop('edgecolors', 'black') - labels = plot_kwargs.pop('labels') + if MPL_GE_3_9: + labels = {'tick_labels': plot_kwargs.pop('tick_labels')} + else: + labels = {'labels': plot_kwargs.pop('labels')} alpha = plot_kwargs.pop('alpha', 1.) showmedians = self.inner == 'medians' bw_method = self.bandwidth or 'scott' @@ -168,7 +175,7 @@ def init_artists(self, ax, plot_args, plot_kwargs): showfliers=False, showcaps=False, patch_artist=True, boxprops={'facecolor': box_color}, medianprops={'color': 'white'}, widths=0.1, - labels=labels) + **labels) artists.update(box) for body, color in zip(artists['bodies'], facecolors): body.set_facecolors(color) @@ -199,7 +206,10 @@ def get_data(self, element, ranges, style): labels.append(label) colors.append(elstyle[i].get('facecolors', 'blue')) style['positions'] = list(range(len(data))) - style['labels'] = labels + if MPL_GE_3_9: + style['tick_labels'] = labels + else: + style['labels'] = labels style['facecolors'] = colors if element.ndims > 0: diff --git a/holoviews/plotting/mpl/util.py b/holoviews/plotting/mpl/util.py index 4699d85907..99c5a9b37c 100644 --- a/holoviews/plotting/mpl/util.py +++ b/holoviews/plotting/mpl/util.py @@ -37,6 +37,8 @@ from ..util import COLOR_ALIASES, RGB_HEX_REGEX mpl_version = Version(mpl.__version__) +MPL_GE_3_7 = mpl_version >= Version('3.7') +MPL_GE_3_9 = mpl_version >= Version('3.9') def is_color(color): diff --git a/holoviews/tests/plotting/matplotlib/test_annotationplot.py b/holoviews/tests/plotting/matplotlib/test_annotationplot.py index d1c1884497..acd7959941 100644 --- a/holoviews/tests/plotting/matplotlib/test_annotationplot.py +++ b/holoviews/tests/plotting/matplotlib/test_annotationplot.py @@ -2,6 +2,7 @@ import holoviews as hv from holoviews.element import HLines, HSpans, VLines, VSpans +from holoviews.plotting.mpl.util import MPL_GE_3_9 from .test_plot import TestMPLPlot, mpl_renderer @@ -153,6 +154,25 @@ def test_vlines_hlines_overlay(self): class TestHVSpansPlot(TestMPLPlot): + + def _hspans_check(self, source, v0, v1): + # Matplotlib 3.9+ uses a rectangle instead of polygon + if MPL_GE_3_9: + rect = [source.get_x(), source.get_y(), source.get_width(), source.get_height()] + assert np.allclose(rect, [0, v0, 1, v1 - v0]) + else: + assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0]) + assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0]) + + def _vspans_check(self, source, v0, v1): + # Matplotlib 3.9+ uses a rectangle instead of polygon + if MPL_GE_3_9: + rect = [source.get_x(), source.get_y(), source.get_width(), source.get_height()] + assert np.allclose(rect, [v0, 0, v1 - v0, 1]) + else: + assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0]) + assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0]) + def test_hspans_plot(self): hspans = HSpans( {"y0": [0, 3, 5.5], "y1": [1, 4, 6.5], "extra": [-1, -2, -3]}, @@ -164,14 +184,17 @@ def test_hspans_plot(self): xlim = plot.handles["fig"].axes[0].get_xlim() ylim = plot.handles["fig"].axes[0].get_ylim() - assert np.allclose(xlim, (-0.055, 0.055)) + if MPL_GE_3_9: + assert np.allclose(xlim, (-0.05, 1.05)) + else: + assert np.allclose(xlim, (-0.055, 0.055)) + assert np.allclose(ylim, (0, 6.5)) sources = plot.handles["annotations"] assert len(sources) == 3 for source, v0, v1 in zip(sources, hspans.data["y0"], hspans.data["y1"]): - assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0]) - assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0]) + self._hspans_check(source, v0, v1) def test_hspans_inverse_plot(self): hspans = HSpans( @@ -190,8 +213,7 @@ def test_hspans_inverse_plot(self): sources = plot.handles["annotations"] assert len(sources) == 3 for source, v0, v1 in zip(sources, hspans.data["y0"], hspans.data["y1"]): - assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0]) - assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0]) + self._vspans_check(source, v0, v1) def test_dynamicmap_overlay_hspans(self): el = HSpans(data=[[1, 3], [2, 4]]) @@ -218,7 +240,10 @@ def test_hspans_nondefault_kdim(self): xlim = plot.handles["fig"].axes[0].get_xlim() ylim = plot.handles["fig"].axes[0].get_ylim() - assert np.allclose(xlim, (-0.055, 0.055)) + if MPL_GE_3_9: + assert np.allclose(xlim, (-0.05, 1.05)) + else: + assert np.allclose(xlim, (-0.055, 0.055)) assert np.allclose(ylim, (0, 6.5)) sources = plot.handles["annotations"] @@ -226,8 +251,7 @@ def test_hspans_nondefault_kdim(self): for source, v0, v1 in zip( sources, hspans.data["other0"], hspans.data["other1"] ): - assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0]) - assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0]) + self._hspans_check(source, v0, v1) def test_vspans_plot(self): vspans = VSpans( @@ -246,8 +270,7 @@ def test_vspans_plot(self): sources = plot.handles["annotations"] assert len(sources) == 3 for source, v0, v1 in zip(sources, vspans.data["x0"], vspans.data["x1"]): - assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0]) - assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0]) + self._vspans_check(source, v0, v1) def test_vspans_inverse_plot(self): vspans = VSpans( @@ -260,14 +283,16 @@ def test_vspans_inverse_plot(self): xlim = plot.handles["fig"].axes[0].get_xlim() ylim = plot.handles["fig"].axes[0].get_ylim() - assert np.allclose(xlim, (-0.055, 0.055)) + if MPL_GE_3_9: + assert np.allclose(xlim, (-0.05, 1.05)) + else: + assert np.allclose(xlim, (-0.055, 0.055)) assert np.allclose(ylim, (0, 6.5)) sources = plot.handles["annotations"] assert len(sources) == 3 for source, v0, v1 in zip(sources, vspans.data["x0"], vspans.data["x1"]): - assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0]) - assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0]) + self._hspans_check(source, v0, v1) def test_vspans_nondefault_kdims(self): vspans = VSpans( @@ -287,8 +312,7 @@ def test_vspans_nondefault_kdims(self): for source, v0, v1 in zip( sources, vspans.data["other0"], vspans.data["other1"] ): - assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0]) - assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0]) + self._vspans_check(source, v0, v1) def test_vspans_hspans_overlay(self): hspans = HSpans( @@ -310,12 +334,10 @@ def test_vspans_hspans_overlay(self): sources = plot.handles["fig"].axes[0].get_children() for source, v0, v1 in zip(sources[:3], hspans.data["y0"], hspans.data["y1"]): - assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0]) - assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0]) + self._hspans_check(source, v0, v1) for source, v0, v1 in zip(sources[3:6], vspans.data["x0"], vspans.data["x1"]): - assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0]) - assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0]) + self._vspans_check(source, v0, v1) def test_dynamicmap_overlay_vspans(self): el = VSpans(data=[[1, 3], [2, 4]]) diff --git a/holoviews/tests/plotting/matplotlib/test_boxwhisker.py b/holoviews/tests/plotting/matplotlib/test_boxwhisker.py index cf28d04abb..2c2ec926b9 100644 --- a/holoviews/tests/plotting/matplotlib/test_boxwhisker.py +++ b/holoviews/tests/plotting/matplotlib/test_boxwhisker.py @@ -1,6 +1,7 @@ import numpy as np from holoviews.element import BoxWhisker +from holoviews.plotting.mpl.util import MPL_GE_3_9 from .test_plot import TestMPLPlot, mpl_renderer @@ -13,7 +14,10 @@ def test_boxwhisker_simple(self): plot = mpl_renderer.get_plot(boxwhisker) data, style, axis_opts = plot.get_data(boxwhisker, {}, {}) self.assertEqual(data[0][0], values) - self.assertEqual(style['labels'], ['']) + if MPL_GE_3_9: + self.assertEqual(style['tick_labels'], ['']) + else: + self.assertEqual(style['labels'], ['']) def test_boxwhisker_simple_overlay(self): values = np.random.rand(100) diff --git a/holoviews/tests/plotting/matplotlib/test_violinplot.py b/holoviews/tests/plotting/matplotlib/test_violinplot.py index 8a1addf252..e87fdb5395 100644 --- a/holoviews/tests/plotting/matplotlib/test_violinplot.py +++ b/holoviews/tests/plotting/matplotlib/test_violinplot.py @@ -1,6 +1,7 @@ import numpy as np from holoviews.element import Violin +from holoviews.plotting.mpl.util import MPL_GE_3_9 from .test_plot import TestMPLPlot, mpl_renderer @@ -14,7 +15,10 @@ def test_violin_simple(self): data, style, axis_opts = plot.get_data(violin, {}, {}) self.assertEqual(data[0][0], values) self.assertEqual(style['positions'], [0]) - self.assertEqual(style['labels'], ['']) + if MPL_GE_3_9: + self.assertEqual(style['tick_labels'], ['']) + else: + self.assertEqual(style['labels'], ['']) def test_violin_simple_overlay(self): values = np.random.rand(100) @@ -34,4 +38,7 @@ def test_violin_multi(self): self.assertEqual(data[0][0], violin.select(A=0).dimension_values(1)) self.assertEqual(data[0][1], violin.select(A=1).dimension_values(1)) self.assertEqual(style['positions'], [0, 1]) - self.assertEqual(style['labels'], ['0', '1']) + if MPL_GE_3_9: + self.assertEqual(style['tick_labels'], ['0', '1']) + else: + self.assertEqual(style['labels'], ['0', '1']) diff --git a/pyproject.toml b/pyproject.toml index 4ff028092b..a95d6e4d5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,8 @@ filterwarnings = [ # 2024-06 "ignore:\\s*Dask dataframe query planning is disabled because dask-expr is not installed:FutureWarning", # OK "ignore:unclosed file <_io.TextIOWrapper name='/dev/null' mode='w':ResourceWarning", # OK + # 2024-07 + "ignore:The (non_)?interactive_bk attribute was deprecated in Matplotlib 3.9", # OK - Only happening in debug mode ] [tool.coverage]