Repository: matplotlib/matplotlib

Issue:
[Bug]: Default ConciseDateFormatter offset_string confusing when axis is inverted
### Bug summary

When an axis with dates is inverted, the offset_string plotted on the bottom right or top left of an axis is counter-intuitive or misleading when using ConciseDateFormatter.  If the axis is inverted, I'd expect either the offset string to be inverted too, or plotted in a different location.  This is especially the case when dates are plotted on the y axis and inverted, but the offset string (plotted next to the top-left tick) is most appropriate for the bottom-left tick.

### Code for reproduction

```Python
# Modeled on https://matplotlib.org/stable/gallery/ticks/date_concise_formatter.html
import datetime

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.dates as mdates

base = datetime.datetime(2005, 2, 1)
dates = [base + datetime.timedelta(hours=(2 * i)) for i in range(732)]
N = len(dates)
np.random.seed(19680801)
y = np.cumsum(np.random.randn(N))

fig, ax = plt.subplots()

ax.plot(y, dates)

lims = (np.datetime64('2005-02-05'), np.datetime64('2005-04'))
ax.set_ylim(lims)
ax.invert_yaxis()

ax.yaxis.set_major_formatter(
    mdates.ConciseDateFormatter(ax.yaxis.get_major_locator()))

plt.show()
```


### Actual outcome

![Figure_1](https://github.com/matplotlib/matplotlib/assets/24902730/55e52e62-f291-4815-af7c-77f295eb0600)


### Expected outcome

![Figure_1_expected](https://github.com/matplotlib/matplotlib/assets/24902730/ce30f7e4-bad5-4c5a-a2ef-e167514b4246)


### Additional information

I'm not aware of any history or context related to this problem.

### Operating system

OS/X

### Matplotlib Version

3.8.2

### Matplotlib Backend

MacOSX

### Python version

Python 3.11.7

### Jupyter version

4.1.0

### Installation

conda


Discussion:
Giving this a quick glance, I think the solution is to modify this code: https://github.com/matplotlib/matplotlib/blob/f5067c16757f4cd800ae3ba0ac25ff79d2e41a7f/lib/matplotlib/dates.py#L797-L803 
If axis.get_inverted() is True, then get the first date from tickdatetime (using tickdatetime[0]) instead of the last date (using tickdatetime[-1]). However I am not sure if this is the best solution or if it could break something else, I will test it.
Is this behaviour different for other types of units on the axis?  Eg does the scalar offset flip if the axis flips? 
Is this what you mean by an axis flip? Here is the same code with ```ax.yaxis.tick_right()``` applied
![Screenshot from 2024-07-01 17-42-45](https://github.com/matplotlib/matplotlib/assets/65249488/04d81654-0c97-4959-bd08-92b313aae274)

> Giving this a quick glance, I think the solution is to modify this code:
> 
> https://github.com/matplotlib/matplotlib/blob/f5067c16757f4cd800ae3ba0ac25ff79d2e41a7f/lib/matplotlib/dates.py#L797-L803
> 
> 
> If axis.get_inverted() is True, then get the first date from tickdatetime (using tickdatetime[0]) instead of the last date (using tickdatetime[-1]). However I am not sure if this is the best solution or if it could break something else, I will test it.

@OdileVidrine: you're spot on here with what I've been thinking too.  And I was able to get the desired plot by modifying [line 799](https://github.com/matplotlib/matplotlib/blob/f5067c16757f4cd800ae3ba0ac25ff79d2e41a7f/lib/matplotlib/dates.py#L799) as you highlighted, exactly as you suggested.  [I hard coded it in a non-flexible way.]. Unfortunately, I'm not too savvy with proper python syntax, OOP, and the use of classes, etc, otherwise I'd implement this fix myself and submit a PR.  :(

Fundamentally, I don't understand enough of how the [format_ticks](https://github.com/matplotlib/matplotlib/blob/f5067c16757f4cd800ae3ba0ac25ff79d2e41a7f/lib/matplotlib/dates.py#L731) function works, or if something about "self" can be asked `axis.get_inverted()`

Your second comment is not what I was meaning by "axis flip," @OdileVidrine.
Thanks for your time
I'm not sure I understand your question, @jklymak.
I fixed the bug here: https://github.com/OdileVidrine/matplotlib/blob/1627770d1f869ebef4266ef215c93084df8e3e4b/lib/matplotlib/dates.py#L797-L806
I'm glad you did a bug report for this, otherwise it probably would have gone unnoticed for a long time.

Super.  Thanks.  Will you do the PR and close this issue?
Yes, after I write a test and check that this change does not break anything else
Sorry, my question was not a good one.  I think checking if the axis is inverted when deciding what to put in the offset is fine.  There was some discussion as to whether it made sense to have this offset in there at all, but it does seem to help many use cases.



Patch:
diff --git a/doc/api/next_api_changes/behavior/28501-OV.rst b/doc/api/next_api_changes/behavior/28501-OV.rst
new file mode 100644
index 000000000000..cc816e55f696
--- /dev/null
+++ b/doc/api/next_api_changes/behavior/28501-OV.rst
@@ -0,0 +1,5 @@
+The offset string associated with ConciseDateFormatter will now invert when the axis is inverted
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Previously, when the axis was inverted, the offset string associated with ConciseDateFormatter would not change,
+so the offset string indicated the axis was oriented in the wrong direction. Now, when the axis is inverted, the offset
+string is oriented correctly.
diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py
index c12d9f31ba4b..15de61f69df7 100644
--- a/lib/matplotlib/dates.py
+++ b/lib/matplotlib/dates.py
@@ -796,7 +796,12 @@ def format_ticks(self, values):
 
         if show_offset:
             # set the offset string:
-            self.offset_string = tickdatetime[-1].strftime(offsetfmts[level])
+            if (self._locator.axis and
+                    self._locator.axis.__name__ in ('xaxis', 'yaxis')
+                    and self._locator.axis.get_inverted()):
+                self.offset_string = tickdatetime[0].strftime(offsetfmts[level])
+            else:
+                self.offset_string = tickdatetime[-1].strftime(offsetfmts[level])
             if self._usetex:
                 self.offset_string = _wrap_in_tex(self.offset_string)
         else:


Test patch:
diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py
index 4133524e0e1a..2d60e3525b2a 100644
--- a/lib/matplotlib/tests/test_dates.py
+++ b/lib/matplotlib/tests/test_dates.py
@@ -636,6 +636,23 @@ def test_concise_formatter_show_offset(t_delta, expected):
     assert formatter.get_offset() == expected
 
 
+def test_concise_formatter_show_offset_inverted():
+    # Test for github issue #28481
+    d1 = datetime.datetime(1997, 1, 1)
+    d2 = d1 + datetime.timedelta(days=60)
+
+    fig, ax = plt.subplots()
+    locator = mdates.AutoDateLocator()
+    formatter = mdates.ConciseDateFormatter(locator)
+    ax.xaxis.set_major_locator(locator)
+    ax.xaxis.set_major_formatter(formatter)
+    ax.invert_xaxis()
+
+    ax.plot([d1, d2], [0, 0])
+    fig.canvas.draw()
+    assert formatter.get_offset() == '1997-Jan'
+
+
 def test_concise_converter_stays():
     # This test demonstrates problems introduced by gh-23417 (reverted in gh-25278)
     # In particular, downstream libraries like Pandas had their designated converters