From a62c549a8896178d39ff2f2609081c652024c1c3 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 14 Apr 2020 06:46:19 -0700 Subject: [PATCH 1/2] ENH: allow title autopositioning to be turned off --- lib/matplotlib/axes/_axes.py | 16 +++++++++++- lib/matplotlib/axes/_base.py | 25 ++++++++----------- .../mpl-data/stylelib/classic.mplstyle | 3 ++- lib/matplotlib/pyplot.py | 4 +-- lib/matplotlib/rcsetup.py | 3 ++- lib/matplotlib/tests/test_axes.py | 2 ++ lib/matplotlib/tests/test_text.py | 5 ++-- lib/matplotlib/tests/test_tightlayout.py | 4 +-- matplotlibrc.template | 3 ++- 9 files changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 3e618964b0c3..afdb9b87458c 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -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. @@ -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. @@ -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 + else: + self._autotitlepos = False + kwargs['y'] = y + titles = {'left': self._left_title, 'center': self.title, 'right': self._right_title} diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 61a0d64cd86f..9b6d82722365 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -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', @@ -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) @@ -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 diff --git a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle index ef5dfa8e3f2a..91e3ed29f28e 100644 --- a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle @@ -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. @@ -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 diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 2a48a9a68262..11391a8e2d57 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -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. diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 8153000a3df2..4d5b9e8718e1 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -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 diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 1eb2eda9abe5..824493402ef6 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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') @@ -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) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index f4b259be3ae4..a348d2fcea01 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -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] @@ -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('') diff --git a/lib/matplotlib/tests/test_tightlayout.py b/lib/matplotlib/tests/test_tightlayout.py index 674cec00b06b..20ec0bbaa1b0 100644 --- a/lib/matplotlib/tests/test_tightlayout.py +++ b/lib/matplotlib/tests/test_tightlayout.py @@ -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() @@ -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 diff --git a/matplotlibrc.template b/matplotlibrc.template index 2596a67f309b..4d59751b629f 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -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. @@ -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 From 30e05cc61018fabd5232679d6bddd036f2482b55 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 14 Apr 2020 20:15:16 -0700 Subject: [PATCH 2/2] DOC --- .../2020-04-14-title-yparam.rst | 8 +++ .../titles_demo.py | 49 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 doc/users/next_whats_new/2020-04-14-title-yparam.rst diff --git a/doc/users/next_whats_new/2020-04-14-title-yparam.rst b/doc/users/next_whats_new/2020-04-14-title-yparam.rst new file mode 100644 index 000000000000..53e98f1fe52b --- /dev/null +++ b/doc/users/next_whats_new/2020-04-14-title-yparam.rst @@ -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`. diff --git a/examples/text_labels_and_annotations/titles_demo.py b/examples/text_labels_and_annotations/titles_demo.py index 5fb5544e9dc4..6d77774246ea 100644 --- a/examples/text_labels_and_annotations/titles_demo.py +++ b/examples/text_labels_and_annotations/titles_demo.py @@ -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. """ @@ -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()