Repository: yt-project/yt

Issue:
BUG: regression with future MPL
### Bug report

**Bug summary**
with #3849 + MPL dev branch, we have one broken test, even after including fixes from #4092

**Code for reproduction**
```shell
pip install git+https://github.com/matplotlib/matplotlib.git
pytest yt/visualization/tests/test_particle_plot.py::test_particle_plot_instance
```

**Actual outcome**

<details><summary> pytest log </summary>

```
================================================ test session starts ================================================
platform darwin -- Python 3.10.3, pytest-7.1.1, pluggy-1.0.0 -- /Users/robcleme/.pyenv/versions/3.10.3/envs/yt-dev/bin/python3.10
cachedir: .pytest_cache
rootdir: /Users/robcleme/dev/yt-project/yt, configfile: pyproject.toml
plugins: anyio-3.5.0, forked-1.4.0, xdist-2.1.0, durations-1.1.0
collected 1 item

yt/visualization/tests/test_particle_plot.py::test_particle_plot_instance yt : [INFO     ] 2022-08-22 20:15:43,056 Parameters: current_time              = 0.0
yt : [INFO     ] 2022-08-22 20:15:43,056 Parameters: domain_dimensions         = [1 1 1]
yt : [INFO     ] 2022-08-22 20:15:43,057 Parameters: domain_left_edge          = [0. 0. 0.]
yt : [INFO     ] 2022-08-22 20:15:43,057 Parameters: domain_right_edge         = [1. 1. 1.]
yt : [INFO     ] 2022-08-22 20:15:43,057 Parameters: cosmological_simulation   = 0
yt : [INFO     ] 2022-08-22 20:15:43,059 Allocating for 4.096e+03 particles
yt : [INFO     ] 2022-08-22 20:15:43,323 xlim = 0.000000 1.000000
yt : [INFO     ] 2022-08-22 20:15:43,323 ylim = 0.000000 1.000000
yt : [INFO     ] 2022-08-22 20:15:43,324 xlim = 0.000000 1.000000
yt : [INFO     ] 2022-08-22 20:15:43,324 ylim = 0.000000 1.000000
yt : [INFO     ] 2022-08-22 20:15:43,326 Splatting (('all', 'particle_ones')) onto a 800 by 800 mesh using method 'ngp'
FAILED

===================================================== FAILURES ======================================================
____________________________________________ test_particle_plot_instance ____________________________________________

    def test_particle_plot_instance():
        """
        Tests the type of plot instance returned by ParticlePlot.

        If x_field and y_field are any combination of valid particle_position in x,
        y or z axis,then ParticleProjectionPlot instance is expected.


        """
        ds = fake_particle_ds()
        x_field = ("all", "particle_position_x")
        y_field = ("all", "particle_position_y")
        z_field = ("all", "particle_velocity_x")

>       plot = ParticlePlot(ds, x_field, y_field)

yt/visualization/tests/test_particle_plot.py:447:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
yt/visualization/particle_plots.py:599: in ParticlePlot
    return ParticleProjectionPlot(ds, direction, z_fields, color, *args, **kwargs)
yt/visualization/particle_plots.py:286: in __init__
    PWViewerMPL.__init__(
yt/visualization/plot_window.py:872: in __init__
    PlotWindow.__init__(self, *args, **kwargs)
yt/visualization/plot_window.py:240: in __init__
    self._setup_plots()
yt/visualization/plot_window.py:1125: in _setup_plots
    self.plots[f] = WindowPlotMPL(
yt/visualization/plot_window.py:2550: in __init__
    self._init_image(data, extent, aspect)
yt/visualization/base_plot_types.py:308: in _init_image
    self.image = self.axes.imshow(
../../../.pyenv/versions/3.10.3/envs/yt-dev/lib/python3.10/site-packages/matplotlib/_api/deprecation.py:454: in wrapper
    return func(*args, **kwargs)
../../../.pyenv/versions/3.10.3/envs/yt-dev/lib/python3.10/site-packages/matplotlib/__init__.py:1422: in inner
    return func(ax, *map(sanitize_sequence, args), **kwargs)
../../../.pyenv/versions/3.10.3/envs/yt-dev/lib/python3.10/site-packages/matplotlib/axes/_axes.py:5565: in imshow
    im._scale_norm(norm, vmin, vmax)
../../../.pyenv/versions/3.10.3/envs/yt-dev/lib/python3.10/site-packages/matplotlib/cm.py:411: in _scale_norm
    self.autoscale_None()
../../../.pyenv/versions/3.10.3/envs/yt-dev/lib/python3.10/site-packages/matplotlib/cm.py:644: in autoscale_None
    self.norm.autoscale_None(self._A)
../../../.pyenv/versions/3.10.3/envs/yt-dev/lib/python3.10/site-packages/matplotlib/colors.py:1720: in autoscale_None
    in_trf_domain = np.extract(np.isfinite(self._trf.transform(A)), A)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <matplotlib.scale.SymmetricalLogTransform object at 0x12323e890>
values = masked_array(
  data=[[0.],
        [0.],
        [0.],
        ...,
        [0.],
        [0.],
        [0.]],
  mask=False,
  fill_value=1e+20)

    def transform(self, values):
        """
        Apply this transformation on the given array of *values*.

        Parameters
        ----------
        values : array
            The input values as NumPy array of length :attr:`input_dims` or
            shape (N x :attr:`input_dims`).

        Returns
        -------
        array
            The output values as NumPy array of length :attr:`output_dims` or
            shape (N x :attr:`output_dims`), depending on the input.
        """
        # Ensure that values is a 2d array (but remember whether
        # we started with a 1d or 2d array).
        values = np.asanyarray(values)
        ndim = values.ndim
        values = values.reshape((-1, self.input_dims))

        # Transform the values
        res = self.transform_affine(self.transform_non_affine(values))

        # Convert the result back to the shape of the input values.
        if ndim == 0:
            assert not np.ma.is_masked(res)  # just to be on the safe side
            return res[0, 0]
        if ndim == 1:
            return res.reshape(-1)
        elif ndim == 2:
            return res
>       raise ValueError(
            "Input values must have shape (N x {dims}) "
            "or ({dims}).".format(dims=self.input_dims))
E       ValueError: Input values must have shape (N x 1) or (1).

../../../.pyenv/versions/3.10.3/envs/yt-dev/lib/python3.10/site-packages/matplotlib/transforms.py:1500: ValueError
```

</details>


Discussion:
The key difference seems to be that here https://github.com/yt-project/yt/blob/379c0826cb1ede4819995f2162d55e8d96cc6eea/yt/visualization/base_plot_types.py#L312

the norm we obtain is different in that test. It used to be a `matplotlib.colors.Normalize` instance, now it's `matplotlib.colors.SymLogNorm`. The reason it's failing to create the image is not that clear however; I suspect the particularity is that a particle plot image data is a 3D array (with separate channels for blue, green, red and alpha), which doesn't work with all norms for some reason. Maybe it's a regression, or an oversight, in matplotlib. I'll dig further


Resolving patch:
diff --git a/yt/visualization/_handlers.py b/yt/visualization/_handlers.py
--- a/yt/visualization/_handlers.py
+++ b/yt/visualization/_handlers.py
@@ -321,7 +321,11 @@ def get_norm(self, data: np.ndarray, *args, **kw) -> Normalize:
             dvmax = 1 * getattr(data, "units", 1)
         kw.setdefault("vmax", dvmax)
 
-        if self.norm_type is not None:
+        if data.ndim == 3:
+            assert data.shape[-1] == 4
+            # this is an RGBA array, only linear normalization makes sense here
+            norm_type = Normalize
+        elif self.norm_type is not None:
             # this is a convenience mechanism for backward compat,
             # allowing to toggle between lin and log scaling without detailed user input
             norm_type = self.norm_type


Test patch: