Repository: matplotlib/matplotlib
Pull request: Offset text position
## PR Summary
fixes #5238 (sets offset text to top when using axis.tick_top())
Solves the issue that the power indicator (e.g. ``1e4``) stays on the bottom, even if the ticks are on the top.

## PR Checklist

- [x] Has Pytest style unit tests
- [x] Code is [Flake 8](http://flake8.pycqa.org/en/latest/) compliant
- [ ] New features are documented, with examples if plot related
- [ ] Documentation is sphinx and numpydoc compliant
- [x] Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
- [ ] Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

<!--
Thank you so much for your PR!  To help us review your contribution, please
consider the following points:

- A development guide is available at https://matplotlib.org/devdocs/devel/index.html.

- Help with git and github is available at
  https://matplotlib.org/devel/gitwash/development_workflow.html.

- Do not create the PR out of master, but out of a separate branch.

- The PR title should summarize the changes, for example "Raise ValueError on
  non-numeric input to set_xlim".  Avoid non-descriptive titles such as
  "Addresses issue #8576".

- The summary should provide at least 1-2 sentences describing the pull request
  in detail (Why is this change required?  What problem does it solve?) and
  link to any relevant issues.

- If you are contributing fixes to docstrings, please pay attention to
  http://matplotlib.org/devel/documenting_mpl.html#formatting.  In particular,
  note the difference between using single backquotes, double backquotes, and
  asterisks in the markup.

We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.
-->

Problem statement:
the offset of the scientific notation in xaxis stays at bottom when axis is set to top
the power indicator of an xaxis set to top stays on the bottom axis

I have seen that for the yaxis you have a function ax.yaxis.set_offset_position('right'), but I could not find any equivalent for the xaxis.

here is an example of the problem.

`import matplotlib.pyplot as plt`
`import numpy as np`

`datay = np.arange(20)`

`datax = np.random.random(20) * 1.e-4`
`ax = plt.subplot(111)`
`ax.plot(datax,datay,c='k')`
`ax.ticklabel_format(axis='x', style='sci', scilimits=(-2,2))`

`data2x = np.random.random(20) * 1.e-4`
`bx = ax.twiny()`
`bx.plot(data2x,datay,c='r')`
`bx.ticklabel_format(axis='x', style='sci', scilimits=(-2,2))`

`plt.show()`

Hints from issue discussion:
confirmed. I could have sworn we saw this bug before and addressed it...

This might require some invasive re-factoring of Axis so tagged for 2.1, if I am wrong and we can do this gently enough can go for 2.0.1.

Is there any workaround for this, e.g., using text? I can't seem to get similar text for the upper axis when using math text (the bottom is not LaTeX). 

I'm still seeing this bug in 3.1.1 (latest master)
Someone could take over #5243?

Commit message: Remove unnecessary .set_va

Patch to review:
diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py
index 5a1778cb311a..046efa50a87a 100644
--- a/lib/matplotlib/axis.py
+++ b/lib/matplotlib/axis.py
@@ -2060,13 +2060,26 @@ def _update_offset_text_position(self, bboxes, bboxes2):
         boxes of all the ticklabels
         """
         x, y = self.offsetText.get_position()
-        if not len(bboxes):
-            bottom = self.axes.bbox.ymin
+        if not hasattr(self, '_text_offset_position'):
+            self._text_offset_position = 'bottom'
+        if self._text_offset_position == 'bottom':
+            if not len(bboxes):
+                bottom = self.axes.bbox.ymin
+            else:
+                bbox = mtransforms.Bbox.union(bboxes)
+                bottom = bbox.y0
+            self.offsetText.set_position(
+                (x, bottom - self.OFFSETTEXTPAD * self.figure.dpi / 72)
+            )
+            return
+        if not len(bboxes2):
+            top = self.axes.bbox.ymax
         else:
-            bbox = mtransforms.Bbox.union(bboxes)
-            bottom = bbox.y0
+            bbox = mtransforms.Bbox.union(bboxes2)
+            top = bbox.y1
+        self.offsetText.set_va('top')
         self.offsetText.set_position(
-            (x, bottom - self.OFFSETTEXTPAD * self.figure.dpi / 72)
+            (x, top + self.OFFSETTEXTPAD * self.figure.dpi / 72)
         )
 
     def get_text_heights(self, renderer):
@@ -2107,9 +2120,11 @@ def set_ticks_position(self, position):
         if position == 'top':
             self.set_tick_params(which='both', top=True, labeltop=True,
                                  bottom=False, labelbottom=False)
+            self._text_offset_position = 'top'
         elif position == 'bottom':
             self.set_tick_params(which='both', top=False, labeltop=False,
                                  bottom=True, labelbottom=True)
+            self._text_offset_position = 'bottom'
         elif position == 'both':
             self.set_tick_params(which='both', top=True,
                                  bottom=True)
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index 16f495c04375..595431a33c5d 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -5201,10 +5201,24 @@ def test_unicode_hist_label():
 
 def test_move_offsetlabel():
     data = np.random.random(10) * 1e-22
+
     fig, ax = plt.subplots()
     ax.plot(data)
+    fig.canvas.draw()
+    before = ax.yaxis.offsetText.get_position()
     ax.yaxis.tick_right()
-    assert (1, 0.5) == ax.yaxis.offsetText.get_position()
+    fig.canvas.draw()
+    after = ax.yaxis.offsetText.get_position()
+    assert after[0] > before[0] and after[1] == before[1]
+
+    fig, ax = plt.subplots()
+    ax.plot(data)
+    fig.canvas.draw()
+    before = ax.xaxis.offsetText.get_position()
+    ax.xaxis.tick_top()
+    fig.canvas.draw()
+    after = ax.xaxis.offsetText.get_position()
+    assert after[0] == before[0] and after[1] > before[1]
 
 
 @image_comparison(['rc_spines.png'], savefig_kwarg={'dpi': 40})


Code review comments:
diff_hunk: @@ -2060,13 +2060,26 @@ def _update_offset_text_position(self, bboxes, bboxes2):
         boxes of all the ticklabels
         """
         x, y = self.offsetText.get_position()
-        if not len(bboxes):
-            bottom = self.axes.bbox.ymin
+        if not hasattr(self, '_text_offset_position'):
+            self._text_offset_position = 'bottom'
+        if self._text_offset_position == 'bottom':
+            if not len(bboxes):
+                bottom = self.axes.bbox.ymin
+            else:
+                bbox = mtransforms.Bbox.union(bboxes)
+                bottom = bbox.y0
+            self.offsetText.set_position(
+                (x, bottom - self.OFFSETTEXTPAD * self.figure.dpi / 72)
+            )
+            return
path: lib/matplotlib/axis.py
text: @user1:
I find the middle-of-the function return makes the function look less symmetric than it actually is.  I think a `else` block would be clearer to read.

diff_hunk: @@ -2107,9 +2120,11 @@ def set_ticks_position(self, position):
         if position == 'top':
             self.set_tick_params(which='both', top=True, labeltop=True,
                                  bottom=False, labelbottom=False)
+            self._text_offset_position = 'top'
         elif position == 'bottom':
             self.set_tick_params(which='both', top=False, labeltop=False,
                                  bottom=True, labelbottom=True)
+            self._text_offset_position = 'bottom'
path: lib/matplotlib/axis.py
text: @user1:
Should we also set this in the `default` and `both` code paths below?  I can see some surprising hysteresis in interactive usage.

@author:
Yes - to be fair, the original PR (#5243) did that, sorry for not having noticed

diff_hunk: @@ -2060,13 +2060,26 @@ def _update_offset_text_position(self, bboxes, bboxes2):
         boxes of all the ticklabels
         """
         x, y = self.offsetText.get_position()
-        if not len(bboxes):
-            bottom = self.axes.bbox.ymin
+        if not hasattr(self, '_text_offset_position'):
+            self._text_offset_position = 'bottom'
path: lib/matplotlib/axis.py
text: @user1:
Suggestion: _text_offset_position -> _tick_position