Repository: scverse/scanpy

Issue:
Unpin matplotlib version once possible
As @fidelram said in https://github.com/theislab/scanpy/pull/661#issuecomment-496144015, we have to wait until matplotlib/matplotlib#14298 is fixed.

It seems to be in matplotlib’s 3.1.2 milestone, so maybe we can just set the dependency to “matplotlib == 3.0.0 or matplotlib >= 3.1.2”

This originally came up in #663, and then later in e.g. #787


Discussion:
This seems to be working now. Given that matplotlib updates are known to introduce bugs, I'm not sure we want to pin matplotlib to `3.3.3`. I'm concerned that's too strong of a restriction, since it's likely there's someone out there who needs to use an older version.

Right now this is causing CI to fail, since there's a canary test that fails when the function works 😆.

Resolving patch:
diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py
--- a/scanpy/plotting/_tools/scatterplots.py
+++ b/scanpy/plotting/_tools/scatterplots.py
@@ -23,7 +23,7 @@
     _FontSize,
     circles,
     ColorLike,
-    make_projection_available,
+    check_projection,
 )
 from .._docs import (
     doc_adata_color_etc,
@@ -112,7 +112,7 @@ def embedding(
     -------
     If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it.
     """
-
+    check_projection(projection)
     sanitize_anndata(adata)
 
     # Setting up color map for continuous values
@@ -140,7 +140,6 @@ def embedding(
         if isinstance(groups, str):
             groups = [groups]
 
-    make_projection_available(projection)
     args_3d = dict(projection='3d') if projection == '3d' else {}
 
     # Deal with Raw
diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py
--- a/scanpy/plotting/_utils.py
+++ b/scanpy/plotting/_utils.py
@@ -6,6 +6,7 @@
 import anndata
 
 import numpy as np
+import matplotlib as mpl
 from matplotlib import pyplot as pl
 from matplotlib import rcParams, ticker, gridspec, axes
 from matplotlib.axes import Axes
@@ -587,7 +588,7 @@ def setup_axes(
     show_ticks=False,
 ):
     """Grid of axes for plotting, legends and colorbars."""
-    make_projection_available(projection)
+    check_projection(projection)
     if left_margin is not None:
         raise NotImplementedError('We currently don’t support `left_margin`.')
     if np.any(colorbars) and right_margin is None:
@@ -1052,36 +1053,18 @@ def data_to_axis_points(ax: Axes, points_data: np.ndarray):
     return data_to_axis(points_data)
 
 
-@lru_cache(None)
-def make_projection_available(projection):
-    avail_projections = {'2d', '3d'}
-    if projection not in avail_projections:
-        raise ValueError(f'choose projection from {avail_projections}')
-    if projection == '2d':
-        return
+def check_projection(projection):
+    """Validation for projection argument."""
+    if projection not in {"2d", "3d"}:
+        raise ValueError(f"Projection must be '2d' or '3d', was '{projection}'.")
+    if projection == "3d":
+        from packaging.version import parse
 
-    from io import BytesIO
-    from matplotlib import __version__ as mpl_version
-    from mpl_toolkits.mplot3d import Axes3D
-
-    fig = Figure()
-    ax = Axes3D(fig)
-
-    circles = PatchCollection([Circle((5, 1)), Circle((2, 2))])
-    ax.add_collection3d(circles, zs=[1, 2])
-
-    buf = BytesIO()
-    try:
-        fig.savefig(buf)
-    except ValueError as e:
-        if not 'operands could not be broadcast together' in str(e):
-            raise e
-        raise ValueError(
-            'There is a known error with matplotlib 3d plotting, '
-            f'and your version ({mpl_version}) seems to be affected. '
-            'Please install matplotlib==3.0.2 or wait for '
-            'https://github.com/matplotlib/matplotlib/issues/14298'
-        )
+        mpl_version = parse(mpl.__version__)
+        if mpl_version < parse("3.3.3"):
+            raise ImportError(
+                f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}"
+            )
 
 
 def circles(x, y, s, ax, marker=None, c='b', vmin=None, vmax=None, **kwargs):


Test patch:
diff --git a/scanpy/tests/_images/master_3dprojection.png b/scanpy/tests/_images/master_3dprojection.png
Binary files a/scanpy/tests/_images/master_3dprojection.png and b/scanpy/tests/_images/master_3dprojection.png differ
diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py
--- a/scanpy/tests/test_plotting.py
+++ b/scanpy/tests/test_plotting.py
@@ -10,6 +10,7 @@
 
 setup()
 
+import matplotlib as mpl
 import matplotlib.pyplot as plt
 import matplotlib.cm as cm
 import numpy as np
@@ -733,8 +734,8 @@ def pbmc_scatterplots():
 def test_scatterplots(image_comparer, pbmc_scatterplots, id, fn):
     save_and_compare_images = image_comparer(ROOT, FIGS, tol=15)
 
-    if id == "3dprojection":
-        # check if this still happens so we can remove our checks once mpl is fixed
+    # https://github.com/theislab/scanpy/issues/849
+    if id == "3dprojection" and version.parse(mpl.__version__) < version.parse("3.3.3"):
         with pytest.raises(ValueError, match=r"known error with matplotlib 3d"):
             fn(pbmc_scatterplots, show=False)
     else: