Repository: pandas-dev/pandas
Pull request: ENH: Fix support for matplotlib's constrained_layout (#25261)
- [x] closes #25261
- [x] tests added / passed
- [x] Ensure all linting tests pass, see [here](https://pandas.pydata.org/pandas-docs/dev/development/contributing.html#code-standards) for how to run them
- [x] whatsnew entry


Problem statement:
plotting: subplots_adjust prevents use of constrained_layout=True
#### Code Sample, a copy-pastable example if possible

```python
# Your code here
import matplotlib.pyplot as plt
import pandas as pd

fig, axes = plt.subplots(2, constrained_layout=True)
times = pd.date_range(start='now', periods=10)
pd.DataFrame({'a': np.arange(10)}, index=times).plot(style='x', ax=axes[0])
```
#### Problem description
Plotting time-series uses pandas-internally a subplots_adjust, but due to this I am unable to use the new matplotlib constrained_layout that would take care of these things automatically.

#### Expected Output
A good layout that respects my constrained_layout setting to plt.subplots()

#### Output of ``pd.show_versions()``

<details>

[paste the output of ``pd.show_versions()`` here below this line]
INSTALLED VERSIONS
------------------
commit: None
python: 3.7.1.final.0
python-bits: 64
OS: Linux
OS-release: 3.10.0-862.el7.x86_64
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.24.1
pytest: 4.2.0
pip: 19.0.2
setuptools: 40.7.3
Cython: 0.29.5
numpy: 1.16.1
scipy: 1.2.0
pyarrow: None
xarray: 0.11.3
IPython: 7.1.1
sphinx: 1.8.4
patsy: 0.5.1
dateutil: 2.8.0
pytz: 2018.9
blosc: None
bottleneck: None
tables: 3.4.4
numexpr: 2.6.9
feather: None
matplotlib: 3.0.2
openpyxl: None
xlrd: 1.2.0
xlwt: 1.3.0
xlsxwriter: 1.1.3
lxml.etree: 4.3.1
bs4: None
html5lib: None
sqlalchemy: 1.2.17
pymysql: None
psycopg2: 2.7.7 (dt dec pq3 ext lo64)
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
gcsfs: None
</details>

Hints from issue discussion:
What does a fix for this involve?

> On Feb 10, 2019, at 18:15, K.-Michael Aye <notifications@github.com> wrote:
> 
> Code Sample, a copy-pastable example if possible
> 
> # Your code here
> import matplotlib.pyplot as plt
> import pandas as pd
> 
> fig, axes = plt.subplots(2, constrained_layout=True)
> times = pd.date_range(start='now', periods=10)
> pd.DataFrame({'a': np.arange(10)}, index=times).plot(style='x', ax=axes[0])
> Problem description
> 
> Plotting time-series uses pandas-internally a subplots_adjust, but due to this I am unable to use the new matplotlib constrained_layout that would take care of these things automatically.
> 
> Expected Output
> 
> A good layout that respects my constrained_layout setting to plt.subplots()
> 
> Output of pd.show_versions()
> 
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub, or mute the thread.

How about a config flag that is being checked that tells if user wants to have subplots adjusted? 



Sent from my iPhone

> On Feb 11, 2019, at 05:48, Tom Augspurger <notifications@github.com> wrote:
> 
> What does a fix for this involve?
> 
> > On Feb 10, 2019, at 18:15, K.-Michael Aye <notifications@github.com> wrote:
> > 
> > Code Sample, a copy-pastable example if possible
> > 
> > # Your code here
> > import matplotlib.pyplot as plt
> > import pandas as pd
> > 
> > fig, axes = plt.subplots(2, constrained_layout=True)
> > times = pd.date_range(start='now', periods=10)
> > pd.DataFrame({'a': np.arange(10)}, index=times).plot(style='x', ax=axes[0])
> > Problem description
> > 
> > Plotting time-series uses pandas-internally a subplots_adjust, but due to this I am unable to use the new matplotlib constrained_layout that would take care of these things automatically.
> > 
> > Expected Output
> > 
> > A good layout that respects my constrained_layout setting to plt.subplots()
> > 
> > Output of pd.show_versions()
> > 
> > —
> > You are receiving this because you are subscribed to this thread.
> > Reply to this email directly, view it on GitHub, or mute the thread.
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub, or mute the thread.

That'd be somewhat unusual for pandas. I don't think we have any other
config options.

Is matplotlib's constrained_layout strictly better than pandas current
stuff? I'd be happy to just let matplotlib take care of it.

On Mon, Feb 11, 2019 at 8:24 AM K.-Michael Aye <notifications@github.com>
wrote:

> How about a config flag that is being checked that tells if user wants to
> have subplots adjusted?
>
>
>
> Sent from my iPhone
>
> > On Feb 11, 2019, at 05:48, Tom Augspurger <notifications@github.com>
> wrote:
> >
> > What does a fix for this involve?
> >
> > > On Feb 10, 2019, at 18:15, K.-Michael Aye <notifications@github.com>
> wrote:
> > >
> > > Code Sample, a copy-pastable example if possible
> > >
> > > # Your code here
> > > import matplotlib.pyplot as plt
> > > import pandas as pd
> > >
> > > fig, axes = plt.subplots(2, constrained_layout=True)
> > > times = pd.date_range(start='now', periods=10)
> > > pd.DataFrame({'a': np.arange(10)}, index=times).plot(style='x',
> ax=axes[0])
> > > Problem description
> > >
> > > Plotting time-series uses pandas-internally a subplots_adjust, but due
> to this I am unable to use the new matplotlib constrained_layout that would
> take care of these things automatically.
> > >
> > > Expected Output
> > >
> > > A good layout that respects my constrained_layout setting to
> plt.subplots()
> > >
> > > Output of pd.show_versions()
> > >
> > > —
> > > You are receiving this because you are subscribed to this thread.
> > > Reply to this email directly, view it on GitHub, or mute the thread.
> > —
> > You are receiving this because you authored the thread.
> > Reply to this email directly, view it on GitHub, or mute the thread.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/25261#issuecomment-462344908>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABQHIqgcRXCfirtd5I8h6FlBO1IV0CTmks5vMX0BgaJpZM4azXjB>
> .
>

Well, when I want to combine several pandas timeseries plots into one figure, this mess happens. It wouldn't happen with constrained_layout, so I'd say, it is strictly better. ;)

<img width="846" alt="Screen Shot 2019-07-16 at 15 52 00" src="https://user-images.githubusercontent.com/69774/61335242-a937d700-a7e1-11e9-9405-b3554e0d9071.png">

The issue is, at the moment pandas uses `subplot_adjust` pandas is robbing the user of the opportunity of letting matplotlib take care of it, b/c matplotlib says, "oh, you adjust layout yourself, then i don't help you laying it out", and there's just no way to avoid it; I find that too imposing by pandas.
In that case, is there any reason to use DataFrame.plot rather than using
Matplotlib directly?

On Tue, Jul 16, 2019 at 5:55 PM K.-Michael Aye <notifications@github.com>
wrote:

> The issue is, at the moment pandas uses subplot_adjust pandas is robbing
> the user of the opportunity of letting matplotlib take care of it, b/c
> matplotlib says, "oh, you adjust layout yourself, then i don't help you
> laying it out".
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/25261?email_source=notifications&email_token=AAKAOITSP6OOZUD3EKJHNHTP7ZGV3A5CNFSM4GWNPDA2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD2CQA3I#issuecomment-512032877>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAKAOIQ3SVR5AEDSGIRZEXDP7ZGV3ANCNFSM4GWNPDAQ>
> .
>

As always, the reason is that somebody else did some coding for you. ;) In this case, it's rather tedious to go through the detail of setting up matplotlib for dates. Or maybe it was, it still was tedious when I started using pandas being so happy that it did plots over time automatically. ;)
Here is an easy to reproduce minimal example:

```python
tseries = pd.date_range('now', freq='s', periods=100)
df = pd.DataFrame({'data': np.random.random(100)}, index=tseries)
df.plot()
```
Output:

<img width="969" alt="Screenshot 2020-02-10 09 55 00" src="https://user-images.githubusercontent.com/69774/74171381-b7b96600-4beb-11ea-8219-9603637589c1.png">

(Also here, for your convenience: https://gist.github.com/0b3c5f2ba247194fe84d8f620278dc7c )

I'm puzzled by the advise to plot using MPL instead of using df.plot(), isn't the functionality there to be used? It's a huge timesaver. Unless you claim I'm the only user that has constrained_layout as default MPL layouter. Do you officially not support plotting with the constraint_layout=True?
`constrained_layout` is typically set at figure creation, so pandas *could* check if its set and *not* do whatever it does to adjust things manually.  Whether it *should* is a cost-benefit question for pandas.  It will probably help some plots, but could break others?

Could you give an example how existing plots could be broken if all pandas could do is to put an if in front of using subplots_adjust? I guess I'm underestimating some hidden complexity.
I'm open to other suggestion, like a wrapper on my side to switch off constrained_layout, although that would mean I either need to wrap all pandas plotters or all non-pandas plotters...

```python
if not constrained_layout:
    do_subplots_adjust
```

Commit message: ENH: Fix support for matplotlib's constrained_layout (#25261)

Patch to review:
diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst
index 05b4417f427ab..9b1afd0c4f95f 100644
--- a/doc/source/whatsnew/v1.3.0.rst
+++ b/doc/source/whatsnew/v1.3.0.rst
@@ -330,7 +330,7 @@ Plotting
 ^^^^^^^^
 
 - Bug in :func:`scatter_matrix` raising when 2d ``ax`` argument passed (:issue:`16253`)
--
+- Support matplotlib's ``constrained_layout=True`` (:issue:`25261`)
 -
 
 Groupby/resample/rolling
diff --git a/pandas/plotting/_matplotlib/boxplot.py b/pandas/plotting/_matplotlib/boxplot.py
index 7122a38db9d0a..8bef8d9ceca98 100644
--- a/pandas/plotting/_matplotlib/boxplot.py
+++ b/pandas/plotting/_matplotlib/boxplot.py
@@ -227,7 +227,8 @@ def _grouped_plot_by_column(
 
     byline = by[0] if len(by) == 1 else by
     fig.suptitle(f"Boxplot grouped by {byline}")
-    fig.subplots_adjust(bottom=0.15, top=0.9, left=0.1, right=0.9, wspace=0.2)
+    if not hasattr(fig, "get_constrained_layout") or not fig.get_constrained_layout():
+        fig.subplots_adjust(bottom=0.15, top=0.9, left=0.1, right=0.9, wspace=0.2)
 
     return result
 
@@ -434,7 +435,11 @@ def boxplot_frame_groupby(
             )
             ax.set_title(pprint_thing(key))
             ret.loc[key] = d
-        fig.subplots_adjust(bottom=0.15, top=0.9, left=0.1, right=0.9, wspace=0.2)
+        if (
+            not hasattr(fig, "get_constrained_layout")
+            or not fig.get_constrained_layout()
+        ):
+            fig.subplots_adjust(bottom=0.15, top=0.9, left=0.1, right=0.9, wspace=0.2)
     else:
         keys, frames = zip(*grouped)
         if grouped.axis == 0:
diff --git a/pandas/plotting/_matplotlib/hist.py b/pandas/plotting/_matplotlib/hist.py
index bff434d3c2d9d..b06aa7a1d5429 100644
--- a/pandas/plotting/_matplotlib/hist.py
+++ b/pandas/plotting/_matplotlib/hist.py
@@ -294,9 +294,10 @@ def plot_group(group, ax):
         axes, xlabelsize=xlabelsize, xrot=xrot, ylabelsize=ylabelsize, yrot=yrot
     )
 
-    fig.subplots_adjust(
-        bottom=0.15, top=0.9, left=0.1, right=0.9, hspace=0.5, wspace=0.3
-    )
+    if not hasattr(fig, "get_constrained_layout") or not fig.get_constrained_layout():
+        fig.subplots_adjust(
+            bottom=0.15, top=0.9, left=0.1, right=0.9, hspace=0.5, wspace=0.3
+        )
     return axes
 
 
@@ -454,6 +455,7 @@ def hist_frame(
     set_ticks_props(
         axes, xlabelsize=xlabelsize, xrot=xrot, ylabelsize=ylabelsize, yrot=yrot
     )
-    fig.subplots_adjust(wspace=0.3, hspace=0.3)
+    if not hasattr(fig, "get_constrained_layout") or not fig.get_constrained_layout():
+        fig.subplots_adjust(wspace=0.3, hspace=0.3)
 
     return axes
diff --git a/pandas/plotting/_matplotlib/misc.py b/pandas/plotting/_matplotlib/misc.py
index 8e81751d88fa1..c8a5f3d7f6bee 100644
--- a/pandas/plotting/_matplotlib/misc.py
+++ b/pandas/plotting/_matplotlib/misc.py
@@ -39,7 +39,8 @@ def scatter_matrix(
     fig, axes = create_subplots(naxes=naxes, figsize=figsize, ax=ax, squeeze=False)
 
     # no gaps between subplots
-    fig.subplots_adjust(wspace=0, hspace=0)
+    if not hasattr(fig, "get_constrained_layout") or not fig.get_constrained_layout():
+        fig.subplots_adjust(wspace=0, hspace=0)
 
     mask = notna(df)
 
@@ -329,7 +330,8 @@ def bootstrap_plot(
     for axis in axes:
         plt.setp(axis.get_xticklabels(), fontsize=8)
         plt.setp(axis.get_yticklabels(), fontsize=8)
-    plt.tight_layout()
+    if not hasattr(fig, "get_constrained_layout") or not fig.get_constrained_layout():
+        plt.tight_layout()
     return fig
 
 
diff --git a/pandas/plotting/_matplotlib/tools.py b/pandas/plotting/_matplotlib/tools.py
index 7440daeb0a632..10735ae1fa857 100644
--- a/pandas/plotting/_matplotlib/tools.py
+++ b/pandas/plotting/_matplotlib/tools.py
@@ -29,7 +29,8 @@ def format_date_labels(ax: "Axes", rot):
         label.set_ha("right")
         label.set_rotation(rot)
     fig = ax.get_figure()
-    fig.subplots_adjust(bottom=0.2)
+    if not hasattr(fig, "get_constrained_layout") or not fig.get_constrained_layout():
+        fig.subplots_adjust(bottom=0.2)
 
 
 def table(


Code review comments:
diff_hunk: @@ -227,7 +227,8 @@ def _grouped_plot_by_column(
 
     byline = by[0] if len(by) == 1 else by
     fig.suptitle(f"Boxplot grouped by {byline}")
-    fig.subplots_adjust(bottom=0.15, top=0.9, left=0.1, right=0.9, wspace=0.2)
+    if not hasattr(fig, "get_constrained_layout") or not fig.get_constrained_layout():
path: pandas/plotting/_matplotlib/boxplot.py
text: @user1:
make a function for this
maybe_adjust_figure or similar

@author:
Done. Hope this is what you had in mind.