Repository: yt-project/yt

Issue:
Change of style in matplotlib
### Bug report

**Bug summary**

Matplotlib 3.4 came with a change in default math text font that affects how yt plots look by default. IMO the "classic" yt look is much nicer so I'd love that it stayed the default style as far as yt is concerned, with no regards to what matplotlib version is installed.

**Code for reproduction**

Here's a simple demo using yt 4.0 and matplotlib 3.4
```python
import yt
import matplotlib.pyplot as plt

def myplot():
    ds = yt.load_sample("IsolatedGalaxy")
    sp = yt.SlicePlot(ds, "z", ("gas", "density"))
    return sp

# default options with matplotlib 3.4
myplot().save("/tmp/defaults")
```

**Actual outcome**

![defaults_Slice_z_density](https://user-images.githubusercontent.com/14075922/133406771-e5d55c50-35ea-4afb-8dc1-a72d4e2b6e0d.png)


**Expected outcome**
This is the "classic yt" look that required no tweaking before Matplotlib 3.4
![classic_yt_Slice_z_density](https://user-images.githubusercontent.com/14075922/133406835-e8631925-d18e-4a35-aa8f-c219bf9b4193.png)

now it can be reproduced with any version of matplotlib with some additional effort on the user side.
```python
plt.rcParams["mathtext.fontset"] = "cm"
myplot().save("/tmp/classic_yt")
```
this is the simplest way to do it, but unfortunately it has side effects, as it won't just change the settings for that one yt plot, but globally change how matplotlib looks through out the session.

Side effects can be avoided using [a context manager](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.rc_context.html)
```python
# with temporary settings (no side effects)
with plt.rc_context({"mathtext.fontset": "cm"}):
    myplot().save("/tmp/classic_yt")
```
but this technique is IMO too disruptive.

I suggest we could use such context manager internally to restore yt's default look with no tweaking on the user side. In order to preserve flexibility (users should still be able to set a different font), some design decisions would be needed.
What do other users and developers think ? 

**Version Information**
  * Operating System: OsX
  * Python Version: 3.9.5
  * yt version: 4.0.1
  * Other Libraries (if applicable): Matplotlib 3.4.2


Discussion:
I do like the old look, and the context manager approach seems right.
Agreed.  I like the old look as well.  Good job in spotting these changes!
Note: I want to research the exact PR that changed that upstream.
What a nice surprise this turned out to be (look who wrote issue 6518 !)

So apparently the change of default math font actually dates back to MPL 2.0, see https://matplotlib.org/2.0.0/users/whats_new.html#change-in-default-font

It is not clear why it only popped up in yt 5 years latter with MPL 3.4, but here is an issue that was _closed_ on MPL 3.4.0
- issue https://github.com/matplotlib/matplotlib/issues/6518
- PR https://github.com/matplotlib/matplotlib/pull/18145 (MPL 3.4.0)

So maybe there's what we need to know there, and it's possible that the change of behaviour wasn't intentional after all and could be considered a MPL bug. I'll investigate this further another time.
The change bisects to https://github.com/matplotlib/matplotlib/pull/18862

Resolving patch:
diff --git a/yt/visualization/_commons.py b/yt/visualization/_commons.py
--- a/yt/visualization/_commons.py
+++ b/yt/visualization/_commons.py
@@ -1,6 +1,9 @@
 import os
 from typing import Optional
 
+import matplotlib
+from packaging.version import Version
+
 from yt.utilities.logger import ytLogger as mylog
 
 from ._mpl_imports import (
@@ -10,6 +13,17 @@
     FigureCanvasSVG,
 )
 
+MPL_VERSION = Version(matplotlib.__version__)
+
+DEFAULT_FONT_PROPERTIES = {
+    "family": "stixgeneral",
+    "size": 18,
+}
+
+if MPL_VERSION >= Version("3.4"):
+    DEFAULT_FONT_PROPERTIES["math_fontfamily"] = "cm"
+
+
 AGG_FORMATS = [".png", ".jpg", ".jpeg", ".raw", ".rgba", ".tif", ".tiff"]
 SUPPORTED_FORMATS = AGG_FORMATS + [".eps", ".ps", ".pdf", ".svg"]
 
diff --git a/yt/visualization/plot_container.py b/yt/visualization/plot_container.py
--- a/yt/visualization/plot_container.py
+++ b/yt/visualization/plot_container.py
@@ -1,13 +1,14 @@
 import base64
 import builtins
 import os
+import sys
 import textwrap
 from collections import defaultdict
 from functools import wraps
 
-import matplotlib
 import numpy as np
 from matplotlib.cm import get_cmap
+from matplotlib.font_manager import FontProperties
 from more_itertools.more import always_iterable
 
 from yt._maintenance.deprecation import issue_deprecation_warning
@@ -19,7 +20,7 @@
 from yt.utilities.definitions import formatted_length_unit_names
 from yt.utilities.exceptions import YTNotInsideNotebook
 
-from ._commons import validate_image_name
+from ._commons import DEFAULT_FONT_PROPERTIES, validate_image_name
 
 try:
     import cmocean
@@ -248,8 +249,6 @@ class PlotContainer:
     _units_config: dict
 
     def __init__(self, data_source, figure_size, fontsize):
-        from matplotlib.font_manager import FontProperties
-
         self.data_source = data_source
         self.ds = data_source.ds
         self.ts = self._initialize_dataset(self.ds)
@@ -257,10 +256,13 @@ def __init__(self, data_source, figure_size, fontsize):
             self.figure_size = float(figure_size[0]), float(figure_size[1])
         else:
             self.figure_size = float(figure_size)
-        font_path = os.path.join(
-            matplotlib.get_data_path(), "fonts", "ttf", "STIXGeneral.ttf"
-        )
-        self._font_properties = FontProperties(size=fontsize, fname=font_path)
+
+        if sys.version_info >= (3, 9):
+            font_dict = DEFAULT_FONT_PROPERTIES | {"size": fontsize}
+        else:
+            font_dict = {**DEFAULT_FONT_PROPERTIES, "size": fontsize}
+
+        self._font_properties = FontProperties(**font_dict)
         self._font_color = None
         self._xlabel = None
         self._ylabel = None
@@ -508,7 +510,6 @@ def set_font(self, font_dict=None):
         ... )
 
         """
-        from matplotlib.font_manager import FontProperties
 
         if font_dict is None:
             font_dict = {}
@@ -516,8 +517,10 @@ def set_font(self, font_dict=None):
             self._font_color = font_dict.pop("color")
         # Set default values if the user does not explicitly set them.
         # this prevents reverting to the matplotlib defaults.
-        font_dict.setdefault("family", "stixgeneral")
-        font_dict.setdefault("size", 18)
+        if sys.version_info >= (3, 9):
+            font_dict = DEFAULT_FONT_PROPERTIES | font_dict
+        else:
+            font_dict = {**DEFAULT_FONT_PROPERTIES, **font_dict}
         self._font_properties = FontProperties(**font_dict)
         return self
 
diff --git a/yt/visualization/profile_plotter.py b/yt/visualization/profile_plotter.py
--- a/yt/visualization/profile_plotter.py
+++ b/yt/visualization/profile_plotter.py
@@ -6,6 +6,7 @@
 
 import matplotlib
 import numpy as np
+from matplotlib.font_manager import FontProperties
 from more_itertools.more import always_iterable, unzip
 from packaging.version import parse as parse_version
 
@@ -17,10 +18,11 @@
 from yt.utilities.logger import ytLogger as mylog
 
 from ..data_objects.selection_objects.data_selection_objects import YTSelectionContainer
-from ._commons import validate_image_name
+from ._commons import DEFAULT_FONT_PROPERTIES, validate_image_name
 from .base_plot_types import ImagePlotMPL, PlotMPL
 from .plot_container import (
     ImagePlotContainer,
+    PlotContainer,
     get_log_minorticks,
     invalidate_plot,
     linear_transform,
@@ -113,7 +115,7 @@ def data_object_or_all_data(data_source):
     return data_source
 
 
-class ProfilePlot:
+class ProfilePlot(PlotContainer):
     r"""
     Create a 1d profile plot from a data source or from a list
     of profile objects.
@@ -374,6 +376,7 @@ def _setup_plots(self):
                     fontproperties=self._font_properties,
                     **self._text_kwargs[f],
                 )
+        self._set_font_properties()
 
         for i, profile in enumerate(self.profiles):
             for field, field_data in profile.items():
@@ -415,9 +418,7 @@ def _initialize_instance(cls, obj, profiles, labels, plot_specs, y_log):
         obj._text_ypos = {}
         obj._text_kwargs = {}
 
-        from matplotlib.font_manager import FontProperties
-
-        obj._font_properties = FontProperties(family="stixgeneral", size=18)
+        obj._font_properties = FontProperties(**DEFAULT_FONT_PROPERTIES)
         obj._font_color = None
         obj.profiles = list(always_iterable(profiles))
         obj.x_log = None
@@ -1336,77 +1337,6 @@ def save(self, name=None, suffix=".png", mpl_kwargs=None):
             self.plots[f].save(name, mpl_kwargs)
         return names
 
-    @invalidate_plot
-    def set_font(self, font_dict=None):
-        """
-
-        Set the font and font properties.
-
-        Parameters
-        ----------
-
-        font_dict : dict
-            A dict of keyword parameters to be passed to
-            :class:`matplotlib.font_manager.FontProperties`.
-
-            Possible keys include:
-
-            * family - The font family. Can be serif, sans-serif, cursive,
-              'fantasy', or 'monospace'.
-            * style - The font style. Either normal, italic or oblique.
-            * color - A valid color string like 'r', 'g', 'red', 'cobalt',
-              and 'orange'.
-            * variant - Either normal or small-caps.
-            * size - Either a relative value of xx-small, x-small, small,
-              medium, large, x-large, xx-large or an absolute font size, e.g. 12
-            * stretch - A numeric value in the range 0-1000 or one of
-              ultra-condensed, extra-condensed, condensed, semi-condensed,
-              normal, semi-expanded, expanded, extra-expanded or ultra-expanded
-            * weight - A numeric value in the range 0-1000 or one of ultralight,
-              light, normal, regular, book, medium, roman, semibold, demibold,
-              demi, bold, heavy, extra bold, or black
-
-            See the matplotlib font manager API documentation for more details.
-            https://matplotlib.org/stable/api/font_manager_api.html
-
-        Notes
-        -----
-
-        Mathtext axis labels will only obey the `size` and `color` keyword.
-
-        Examples
-        --------
-
-        This sets the font to be 24-pt, blue, sans-serif, italic, and
-        bold-face.
-
-        >>> prof = ProfilePlot(
-        ...     ds.all_data(), ("gas", "density"), ("gas", "temperature")
-        ... )
-        >>> slc.set_font(
-        ...     {
-        ...         "family": "sans-serif",
-        ...         "style": "italic",
-        ...         "weight": "bold",
-        ...         "size": 24,
-        ...         "color": "blue",
-        ...     }
-        ... )
-
-        """
-        from matplotlib.font_manager import FontProperties
-
-        if font_dict is None:
-            font_dict = {}
-        if "color" in font_dict:
-            self._font_color = font_dict.pop("color")
-        # Set default values if the user does not explicitly set them.
-        # this prevents reverting to the matplotlib defaults.
-        font_dict.setdefault("family", "stixgeneral")
-        font_dict.setdefault("size", 18)
-        self._font_properties = FontProperties(**font_dict)
-        return self
-
     @invalidate_plot
     def set_title(self, field, title):
         """Set a title for the plot.


Test patch:
diff --git a/tests/matplotlibrc b/tests/matplotlibrc
--- a/tests/matplotlibrc
+++ b/tests/matplotlibrc
@@ -1,4 +1,3 @@
 #### MATPLOTLIBRC FORMAT
 
 backend : Agg
-mathtext.fontset : cm
diff --git a/tests/tests.yaml b/tests/tests.yaml
--- a/tests/tests.yaml
+++ b/tests/tests.yaml
@@ -92,7 +92,7 @@ answer_tests:
     - yt/frontends/owls/tests/test_outputs.py:test_snapshot_033
     - yt/frontends/owls/tests/test_outputs.py:test_OWLS_particlefilter
 
-  local_pw_037:  # PR 3373
+  local_pw_038:  # PR 3520
     - yt/visualization/tests/test_plotwindow.py:test_attributes
     - yt/visualization/tests/test_particle_plot.py:test_particle_projection_answers
     - yt/visualization/tests/test_particle_plot.py:test_particle_projection_filter
@@ -167,7 +167,7 @@ answer_tests:
   local_axialpix_007:
     - yt/geometry/coordinates/tests/test_axial_pixelization.py:test_axial_pixelization
 
-  local_cylindrical_background_008:  # PR 3536
+  local_cylindrical_background_010:  # PR 3520
     - yt/geometry/coordinates/tests/test_cylindrical_coordinates.py:test_noise_plots
 
   #local_particle_trajectory_001: