Skip to content

Move title up if x-axis is on the top of the figure #9498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions doc/users/next_whats_new/title_will_not_overlap_xaxis.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Axes title will no longer overlap xaxis
---------------------------------------

Previously an axes title had to be moved manually if an xaxis overlapped
(usually when the xaxis was put on the top of the axes). Now, the title
will be automatically moved above the xaxis and its decorators (including
the xlabel) if they are at the top.

If desired, the title can still be placed manually. There is a slight kludge;
the algorithm checks if the y-position of the title is 1.0 (the default),
and moves if it is. If the user places the title in the default location
(i.e. ``ax.title.set_position(0.5, 1.0)``), the title will still be moved
above the xaxis. If the user wants to avoid this, they can
specify a number that is close (i.e. ``ax.title.set_position(0.5, 1.01)``)
and the title will not be moved via this algorithm.
2 changes: 1 addition & 1 deletion examples/ticks_and_spines/tick_xlabel_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
fig, ax = plt.subplots()

ax.plot(x)
ax.set_title('xlabel top', pad=24) # increase padding to make room for labels
ax.set_title('xlabel top') # Note title moves to make room for ticks

plt.show()
63 changes: 59 additions & 4 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections import OrderedDict
import itertools
import logging
import math
from operator import attrgetter
import types
Expand Down Expand Up @@ -32,6 +33,8 @@
from matplotlib.rcsetup import cycler
from matplotlib.rcsetup import validate_axisbelow

_log = logging.getLogger(__name__)

rcParams = matplotlib.rcParams


Expand Down Expand Up @@ -1077,6 +1080,8 @@ def cla(self):
# refactor this out so it can be called in ax.set_title if
# pad argument used...
self._set_title_offset_trans(title_offset_points)
# determine if the title position has been set manually:
self._autotitlepos = None

for _title in (self.title, self._left_title, self._right_title):
self._set_artist_props(_title)
Expand Down Expand Up @@ -2446,6 +2451,50 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval,
def _get_axis_list(self):
return (self.xaxis, self.yaxis)

def _update_title_position(self, renderer):
"""
Update the title position based on the bounding box enclosing
all the ticklabels and x-axis spine and xlabel...
"""
_log.debug('update_title_pos')

if self._autotitlepos is not None and not self._autotitlepos:
_log.debug('title position was updated manually, not adjusting')
return

titles = (self.title, self._left_title, self._right_title)

if self._autotitlepos is None:
for title in titles:
x, y = title.get_position()
if not np.isclose(y, 1.0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very dumb question incoming 🐑... With this implementation, there is no way to pin the title y-position at 1.0 and still avoid the auto title positioning, is there not? But in case anybody would like to do so (for whichever good or bad reason ^^), I guess that a reasonable workaround would be to pin the title y-position at something like 1.001, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats correct. I could do something like find the first call that sets the title at 1.0 and then flag future calls to set_position as being "pinned", even if it is y=1.0. But setting to 1.01 will give the same result.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, setting y=1.001 is a good enough solution. Maybe it should be put in the “what's new” entry (just for the workaround to be documented somewhere)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

self._autotitlepos = False
_log.debug('not adjusting title pos because title was'
' already placed manually: %f', y)
return
self._autotitlepos = True

for title in titles:
x, y0 = title.get_position()
y = 1.0
# need to check all our twins too...
axs = self._twinned_axes.get_siblings(self)

for ax in axs:
try:
if (ax.xaxis.get_label_position() == 'top'
or ax.xaxis.get_ticks_position() == 'top'):
bb = ax.xaxis.get_tightbbox(renderer)
top = bb.ymax
# we don't need to pad because the padding is already
# in __init__: titleOffsetTrans
yn = self.transAxes.inverted().transform((0., top))[1]
y = max(y, yn)
except AttributeError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By curiosity, when is an AttributeError likely to occur? In a case where an empty boundary box would be returned (I mean a case where bb would be None)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ummm, I forget why exactly, but I think it was possible for ax to not have an xaxis property. I can check what test fails if important.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough for me, no worry ;).

pass

title.set_position((x, y))

# Drawing

@allow_rasterization
Expand All @@ -2459,6 +2508,7 @@ def draw(self, renderer=None, inframe=False):
if not self.get_visible():
return
renderer.open_group('axes')

# prevent triggering call backs during the draw process
self._stale = True
locator = self.get_axes_locator()
Expand All @@ -2479,6 +2529,8 @@ def draw(self, renderer=None, inframe=False):
for spine in self.spines.values():
artists.remove(spine)

self._update_title_position(renderer)

if self.axison and not inframe:
if self._axisbelow is True:
self.xaxis.set_zorder(0.5)
Expand Down Expand Up @@ -2507,6 +2559,7 @@ def draw(self, renderer=None, inframe=False):
# rasterize artists with negative zorder
# if the minimum zorder is negative, start rasterization
rasterization_zorder = self._rasterization_zorder

if (rasterization_zorder is not None and
artists and artists[0].zorder < rasterization_zorder):
renderer.start_rasterizing()
Expand Down Expand Up @@ -4051,6 +4104,12 @@ def get_tightbbox(self, renderer, call_axes_locator=True):
else:
self.apply_aspect()

bb_xaxis = self.xaxis.get_tightbbox(renderer)
if bb_xaxis:
bb.append(bb_xaxis)

self._update_title_position(renderer)

bb.append(self.get_window_extent(renderer))

if self.title.get_visible():
Expand All @@ -4060,10 +4119,6 @@ def get_tightbbox(self, renderer, call_axes_locator=True):
if self._right_title.get_visible():
bb.append(self._right_title.get_window_extent(renderer))

bb_xaxis = self.xaxis.get_tightbbox(renderer)
if bb_xaxis:
bb.append(bb_xaxis)

bb_yaxis = self.yaxis.get_tightbbox(renderer)
if bb_yaxis:
bb.append(bb_yaxis)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5411,6 +5411,42 @@ def test_axisbelow():
ax.set_axisbelow(setting)


@image_comparison(baseline_images=['titletwiny'], style='mpl20',
extensions=['png'])
def test_titletwiny():
# Test that title is put above xlabel if xlabel at top
fig, ax = plt.subplots()
fig.subplots_adjust(top=0.8)
ax2 = ax.twiny()
ax.set_xlabel('Xlabel')
ax2.set_xlabel('Xlabel2')
ax.set_title('Title')


def test_titlesetpos():
# Test that title stays put if we set it manually
fig, ax = plt.subplots()
fig.subplots_adjust(top=0.8)
ax2 = ax.twiny()
ax.set_xlabel('Xlabel')
ax2.set_xlabel('Xlabel2')
ax.set_title('Title')
pos = (0.5, 1.11)
ax.title.set_position(pos)
renderer = fig.canvas.get_renderer()
ax._update_title_position(renderer)
assert ax.title.get_position() == pos


def test_title_xticks_top():
# Test that title moves if xticks on top of axes.
fig, ax = plt.subplots()
ax.xaxis.set_ticks_position('top')
ax.set_title('xlabel top')
fig.canvas.draw()
assert ax.title.get_position()[1] > 1.04
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just by curiosity, is 1.04 an arbitrary threshold value that was empirically found? (I would have genuinely expected something like > 1.0, or maybe > 1.01 to avoid numerical precision issues 🐑 )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.04 was just large enough to definitely not be at 1.0. No good reason for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, I just wanted to be sure that I was not missing something.



def test_offset_label_color():
# Tests issue 6440
fig = plt.figure()
Expand Down