Skip to content

ENH: allow title autopositioning to be turned off #17127

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
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
8 changes: 8 additions & 0 deletions doc/users/next_whats_new/2020-04-14-title-yparam.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
`~.axes.Axes.set_title` gains a y keyword argument to control auto positioning
------------------------------------------------------------------------------
`~.axes.Axes.set_title` tries to auto-position the title to avoid any
decorators on the top x-axis. This is not always desirable so now
*y* is an explicit keyword argument of `~.axes.Axes.set_title`. It
defaults to *None* which means to use auto-positioning. If a value is
supplied (i.e. the pre-3.0 default was ``y=1.0``) then auto-positioning is
turned off. This can also be set with the new rcParameter :rc:`axes.titley`.
49 changes: 45 additions & 4 deletions examples/text_labels_and_annotations/titles_demo.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""
===========
Titles Demo
===========
=================
Title positioning
=================

matplotlib can display plot titles centered, flush with the left side of
Matplotlib can display plot titles centered, flush with the left side of
a set of axes, and flush with the right side of a set of axes.

"""
Expand All @@ -16,3 +16,44 @@
plt.title('Right Title', loc='right')

plt.show()

###########################################################################
# The vertical position is automatically chosen to avoid decorations
# (i.e. labels and ticks) on the topmost x-axis:

fig, axs = plt.subplots(1, 2, constrained_layout=True)

ax = axs[0]
ax.plot(range(10))
ax.xaxis.set_label_position('top')
ax.set_xlabel('X-label')
ax.set_title('Center Title')

ax = axs[1]
ax.plot(range(10))
ax.xaxis.set_label_position('top')
ax.xaxis.tick_top()
ax.set_xlabel('X-label')
ax.set_title('Center Title')
plt.show()

###########################################################################
# Automatic positioning can be turned off by manually specifying the
# *y* kwarg for the title or setting :rc:`axes.titley` in the rcParams.

fig, axs = plt.subplots(1, 2, constrained_layout=True)

ax = axs[0]
ax.plot(range(10))
ax.xaxis.set_label_position('top')
ax.set_xlabel('X-label')
ax.set_title('Manual y', y=1.0, pad=-14)

plt.rcParams['axes.titley'] = 1.0 # y is in axes-relative co-ordinates.
plt.rcParams['axes.titlepad'] = -14 # pad is in points...
ax = axs[1]
ax.plot(range(10))
ax.set_xlabel('X-label')
ax.set_title('rcParam y')

plt.show()
16 changes: 15 additions & 1 deletion lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ def get_title(self, loc="center"):
title = cbook._check_getitem(titles, loc=loc.lower())
return title.get_text()

def set_title(self, label, fontdict=None, loc=None, pad=None, **kwargs):
def set_title(self, label, fontdict=None, loc=None, pad=None, *, y=None,
**kwargs):
"""
Set a title for the axes.

Expand All @@ -140,6 +141,11 @@ def set_title(self, label, fontdict=None, loc=None, pad=None, **kwargs):
loc : {'center', 'left', 'right'}, default: :rc:`axes.titlelocation`
Which title to set.

y : float, default: :rc:`axes.titley`
Vertical axes loation for the title (1.0 is the top). If
None (the default), y is determined automatically to avoid
decorators on the axes.

pad : float, default: :rc:`axes.titlepad`
The offset of the title from the top of the axes, in points.

Expand All @@ -157,6 +163,14 @@ def set_title(self, label, fontdict=None, loc=None, pad=None, **kwargs):
if loc is None:
loc = rcParams['axes.titlelocation']

if y is None:
y = rcParams['axes.titley']
if y is None:
y = 1.0

Choose a reason for hiding this comment

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

Do you need self._autotitlepos = True here?

Copy link
Member Author

@jklymak jklymak Apr 15, 2020

Choose a reason for hiding this comment

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

The problem here is that there are really three titles with one interface. So setting self._autotitlepos=True here would allow toggling the automatic behaviour, but that behaviour would also affect other titles. i.e.

ax.set_title('Center', y=1.0)
ax.set_title('Left', loc='left')

would turn auto positioning on again for both titles.

I think the current proposed behaviour is if you make it manual, it stays manual, leads to the least unexpected results. We could make it all more complicated, but I really don't envision folks toggling this.

Choose a reason for hiding this comment

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

Ok, that might still confuse someone, but you're right, the alternative would be more problematic because people might want to position the different titles differently.

else:
self._autotitlepos = False
kwargs['y'] = y

titles = {'left': self._left_title,
'center': self.title,
'right': self._right_title}
Expand Down
25 changes: 10 additions & 15 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,19 +1122,26 @@ def cla(self):
size=mpl.rcParams['axes.titlesize'],
weight=mpl.rcParams['axes.titleweight'])

y = mpl.rcParams['axes.titley']
if y is None:
y = 1.0
self._autotitlepos = True
else:
self._autotitlepos = False

self.title = mtext.Text(
x=0.5, y=1.0, text='',
x=0.5, y=y, text='',
fontproperties=props,
verticalalignment='baseline',
horizontalalignment='center',
)
self._left_title = mtext.Text(
x=0.0, y=1.0, text='',
x=0.0, y=y, text='',
fontproperties=props.copy(),
verticalalignment='baseline',
horizontalalignment='left', )
self._right_title = mtext.Text(
x=1.0, y=1.0, text='',
x=1.0, y=y, text='',
fontproperties=props.copy(),
verticalalignment='baseline',
horizontalalignment='right',
Expand All @@ -1143,8 +1150,6 @@ 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 @@ -2622,16 +2627,6 @@ def _update_title_position(self, renderer):

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):
self._autotitlepos = False
_log.debug('not adjusting title pos because a title was '
'already placed manually: %f', y)
return
self._autotitlepos = True

for title in titles:
x, _ = title.get_position()
# need to start again in case of window resizing
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/mpl-data/stylelib/classic.mplstyle
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ mathtext.sf : sans\-serif
mathtext.fontset : cm # Should be 'cm' (Computer Modern), 'stix',
# 'stixsans' or 'custom'
mathtext.fallback: cm # Select fallback font from ['cm' (Computer Modern), 'stix'
# 'stixsans'] when a symbol can not be found in one of the
# 'stixsans'] when a symbol can not be found in one of the
# custom math fonts. Select 'None' to not perform fallback
# and replace the missing character by a dummy.

Expand All @@ -175,6 +175,7 @@ axes.grid : False # display grid or not
axes.grid.which : major
axes.grid.axis : both
axes.titlesize : large # fontsize of the axes title
axes.titley : 1.0 # at the top, no autopositioning.
axes.titlepad : 5.0 # pad between axes and title in points
axes.titleweight : normal # font weight for axes title
axes.labelsize : medium # fontsize of the x any y labels
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2931,9 +2931,9 @@ def sci(im):

# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
@_copy_docstring_and_deprecators(Axes.set_title)
def title(label, fontdict=None, loc=None, pad=None, **kwargs):
def title(label, fontdict=None, loc=None, pad=None, *, y=None, **kwargs):
return gca().set_title(
label, fontdict=fontdict, loc=loc, pad=pad, **kwargs)
label, fontdict=fontdict, loc=loc, pad=pad, y=y, **kwargs)


# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,8 @@ def _convert_validator_spec(key, conv):
'axes.titlelocation': ['center', ['left', 'center', 'right']], # alignment of axes title
'axes.titleweight': ['normal', validate_fontweight], # font weight of axes title
'axes.titlecolor': ['auto', validate_color_or_auto], # font color of axes title
'axes.titlepad': [6.0, validate_float], # pad from axes top to title in points
'axes.titley': [None, validate_float_or_None], # title location, axes units, None means auto
'axes.titlepad': [6.0, validate_float], # pad from axes top decoration to title in points
'axes.grid': [False, validate_bool], # display grid or not
'axes.grid.which': ['major', ['minor', 'both', 'major']], # set whether the grid is drawn on
# 'major' 'minor' or 'both' ticks
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5522,6 +5522,7 @@ def test_titlesetpos():

def test_title_xticks_top():
# Test that title moves if xticks on top of axes.
mpl.rcParams['axes.titley'] = None
fig, ax = plt.subplots()
ax.xaxis.set_ticks_position('top')
ax.set_title('xlabel top')
Expand All @@ -5531,6 +5532,7 @@ def test_title_xticks_top():

def test_title_xticks_top_both():
# Test that title moves if xticks on top of axes.
mpl.rcParams['axes.titley'] = None
fig, ax = plt.subplots()
ax.tick_params(axis="x",
bottom=True, top=True, labelbottom=True, labeltop=True)
Expand Down
5 changes: 2 additions & 3 deletions lib/matplotlib/tests/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ def test_annotation_units(fig_test, fig_ref):
def test_large_subscript_title():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
plt.rcParams['axes.titley'] = None

fig, axs = plt.subplots(1, 2, figsize=(9, 2.5), constrained_layout=True)
ax = axs[0]
Expand All @@ -626,9 +627,7 @@ def test_large_subscript_title():
ax.set_xticklabels('')

ax = axs[1]
tt = ax.set_title(r'$\sum_{i} x_i$')
x, y = tt.get_position()
tt.set_position((x, 1.01))
tt = ax.set_title(r'$\sum_{i} x_i$', y=1.01)
ax.set_title('Old Way', loc='left')
ax.set_xticklabels('')

Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_tightlayout.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def example_plot(ax, fontsize=12):
ax.set_title('Title', fontsize=fontsize)


@image_comparison(['tight_layout1'])
@image_comparison(['tight_layout1'], tol=1.9)
def test_tight_layout1():
"""Test tight_layout for a single subplot."""
fig, ax = plt.subplots()
Expand Down Expand Up @@ -115,7 +115,7 @@ def test_tight_layout6():
h_pad=0.45)


@image_comparison(['tight_layout7'])
@image_comparison(['tight_layout7'], tol=1.9)
def test_tight_layout7():
# tight layout with left and right titles
fontsize = 24
Expand Down
3 changes: 2 additions & 1 deletion matplotlibrc.template
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@
#mathtext.sf: sans
#mathtext.tt: monospace
#mathtext.fallback: cm # Select fallback font from ['cm' (Computer Modern), 'stix'
# 'stixsans'] when a symbol can not be found in one of the
# 'stixsans'] when a symbol can not be found in one of the
# custom math fonts. Select 'None' to not perform fallback
# and replace the missing character by a dummy symbol.
#mathtext.default: it # The default font to use for math.
Expand All @@ -352,6 +352,7 @@
#axes.titleweight: normal # font weight of title
#axes.titlecolor: auto # color of the axes title, auto falls back to
# text.color as default value
#axes.titley: None # position title (axes relative units). None implies auto
#axes.titlepad: 6.0 # pad between axes and title in points
#axes.labelsize: medium # fontsize of the x any y labels
#axes.labelpad: 4.0 # space between label and axis
Expand Down