Skip to content

Commit

Permalink
comp(matplotlib): Add compatibility with Matplotlib 3.9 (#6307)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro authored Jul 4, 2024
1 parent e5f7aed commit eb79ab5
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 30 deletions.
15 changes: 11 additions & 4 deletions holoviews/plotting/mpl/chart.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions holoviews/plotting/mpl/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions holoviews/plotting/mpl/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
60 changes: 41 additions & 19 deletions holoviews/tests/plotting/matplotlib/test_annotationplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]},
Expand All @@ -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(
Expand All @@ -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]])
Expand All @@ -218,16 +240,18 @@ 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"]
assert len(sources) == 3
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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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]])
Expand Down
6 changes: 5 additions & 1 deletion holoviews/tests/plotting/matplotlib/test_boxwhisker.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand Down
11 changes: 9 additions & 2 deletions holoviews/tests/plotting/matplotlib/test_violinplot.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand All @@ -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'])
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit eb79ab5

Please sign in to comment.