From 61b6cdcbda6f06d9f9e379184712e5678fc7af13 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 29 Jul 2025 19:23:50 +0200 Subject: [PATCH] Support standard tickdir control (in/out/inout) in axisartist. ... and also respect rcParams, which makes axisartist ticks default to pointing outwards, similarly to standard ticks. Rework the tick direction machinery to also support "inout" (similarly to standard ticks). One may wonder whether external ticks should really be drawn in the local direction of the gridlines, though, or whether drawing them orthogonal to the axis spine would look better. This PR keeps the old behavior of `set_ticks_out(True)`, but we could later add something like ``set_tick_direction("out_ortho")`` (name up to bikeshedding). While at it, also deprecate the entirely unused `Ticks.locs_angles_labels` and `LabelBase.locs_angles_labels`. --- .../deprecations/30369-AL.rst | 14 +++ .../examples/axisartist/simple_axis_pad.py | 28 +++-- .../users_explain/toolkits/axisartist.rst | 50 ++++----- lib/mpl_toolkits/axisartist/axis_artist.py | 100 ++++++++++++------ .../axisartist/tests/test_axis_artist.py | 4 +- .../axisartist/tests/test_axislines.py | 16 ++- .../axisartist/tests/test_floating_axes.py | 5 +- .../tests/test_grid_helper_curvelinear.py | 9 +- 8 files changed, 151 insertions(+), 75 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/30369-AL.rst diff --git a/doc/api/next_api_changes/deprecations/30369-AL.rst b/doc/api/next_api_changes/deprecations/30369-AL.rst new file mode 100644 index 000000000000..8b82c80054d9 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30369-AL.rst @@ -0,0 +1,14 @@ +:mod:`.axisartist` now uses more standard tick direction controls +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, the position of :mod:`.axisartist` ticks (inside or outside +the axes) were set using ``set_tick_out(bool)``. They are now set +using ``set_tick_direction("in")`` (or "out", or "inout"), and respect +:rc:`xtick.direction` and :rc:`ytick.direction`. In particular, they default +to pointing outwards, consistently with the rest of the library. + +The *tick_out* parameter of `.Ticks` has been deprecated (use *tick_direction* +instead). The ``Ticks.get_tick_out`` method is deprecated (use +`.Ticks.get_tick_direction` instead). + +The unused ``locs_angles_labels`` attribute of `.Ticks` and `.LabelBase` has +also been deprecated. diff --git a/galleries/examples/axisartist/simple_axis_pad.py b/galleries/examples/axisartist/simple_axis_pad.py index f436ae3ab79c..ea4415eff61d 100644 --- a/galleries/examples/axisartist/simple_axis_pad.py +++ b/galleries/examples/axisartist/simple_axis_pad.py @@ -63,23 +63,33 @@ def ann(ax1, d): va="top", ha="center") -ax1 = setup_axes(fig, rect=141) +ax1 = setup_axes(fig, rect=231) axis = add_floating_axis1(ax1) -ann(ax1, r"default") +ann(ax1, "default") -ax1 = setup_axes(fig, rect=142) +ax1 = setup_axes(fig, rect=232) axis = add_floating_axis1(ax1) axis.major_ticklabels.set_pad(10) -ann(ax1, r"ticklabels.set_pad(10)") +ann(ax1, "ticklabels.set_pad(10)") -ax1 = setup_axes(fig, rect=143) +ax1 = setup_axes(fig, rect=233) axis = add_floating_axis1(ax1) axis.label.set_pad(20) -ann(ax1, r"label.set_pad(20)") +ann(ax1, "label.set_pad(20)") -ax1 = setup_axes(fig, rect=144) +ax1 = setup_axes(fig, rect=234) axis = add_floating_axis1(ax1) -axis.major_ticks.set_tick_out(True) -ann(ax1, "ticks.set_tick_out(True)") +axis.major_ticks.set_tick_direction("in") +ann(ax1, 'ticks.set_tick_direction("in")') + +ax1 = setup_axes(fig, rect=235) +axis = add_floating_axis1(ax1) +axis.major_ticks.set_tick_direction("out") +ann(ax1, 'ticks.set_tick_direction("out")') + +ax1 = setup_axes(fig, rect=236) +axis = add_floating_axis1(ax1) +axis.major_ticks.set_tick_direction("inout") +ann(ax1, 'ticks.set_tick_direction("inout")') plt.show() diff --git a/galleries/users_explain/toolkits/axisartist.rst b/galleries/users_explain/toolkits/axisartist.rst index a5a1b6cbcd78..6fad0ee6a731 100644 --- a/galleries/users_explain/toolkits/axisartist.rst +++ b/galleries/users_explain/toolkits/axisartist.rst @@ -287,16 +287,16 @@ HowTo ax.axis[:].major_ticklabels.set_color("r") -4. To change the tick size (length), you need to use - axis.major_ticks.set_ticksize method. To change the direction of - the ticks (ticks are in opposite direction of ticklabels by - default), use axis.major_ticks.set_tick_out method. +4. To change the tick size (length), use ``axis.major_ticks.set_ticksize``. + + To change the direction of the ticks, use + ``axis.major_ticks.set_tick_direction``. To change the pad between ticks and ticklabels, use - axis.major_ticklabels.set_pad method. + ``axis.major_ticklabels.set_pad``. - To change the pad between ticklabels and axis label, - axis.label.set_pad method. + To change the pad between ticklabels and axis label, use + ``axis.label.set_pad``. Rotation and alignment of TickLabels ==================================== @@ -398,15 +398,14 @@ axis_direction of ticks, ticklabels, and axis-label does not affect them. If you want to make ticks outward and ticklabels inside the axes, -use invert_ticklabel_direction method. :: +use `.AxisArtist.invert_ticklabel_direction`:: ax.axis[:].invert_ticklabel_direction() -A related method is "set_tick_out". It makes ticks outward (as a -matter of fact, it makes ticks toward the opposite direction of the -default direction). :: +A related method is `.Ticks.set_tick_direction`. It can make ticks point "in", +"out", or "inout" (crossing the axis halfway):: - ax.axis[:].major_ticks.set_tick_out(True) + ax.axis[:].major_ticks.set_tick_direction("inout") .. figure:: /gallery/axisartist/images/sphx_glr_simple_axis_direction03_001.png :target: /gallery/axisartist/simple_axis_direction03.html @@ -416,27 +415,28 @@ So, in summary, * AxisArtist's methods - - set_axis_direction: "left", "right", "bottom", or "top" - - set_ticklabel_direction: "+" or "-" - - set_axislabel_direction: "+" or "-" - - invert_ticklabel_direction + - `~.AxisArtist.set_axis_direction`: "left", "right", "bottom", or "top" + - `~.AxisArtist.set_ticklabel_direction`: "+" or "-" + - `~.AxisArtist.set_axislabel_direction`: "+" or "-" + - `~.AxisArtist.invert_ticklabel_direction` * Ticks' methods (major_ticks and minor_ticks) - - set_tick_out: True or False - - set_ticksize: size in points + - `~.Ticks.set_tick_direction`: "in", "out", or "inout" + - `~.Ticks.set_ticksize`: size in points * TickLabels' methods (major_ticklabels and minor_ticklabels) - - set_axis_direction: "left", "right", "bottom", or "top" - - set_rotation: angle with respect to the reference direction - - set_ha and set_va: see below + - `~.TickLabels.set_axis_direction`: "left", "right", "bottom", or "top" + - `~.Text.set_rotation`: angle with respect to the reference direction + - `~.Text.set_horizontalalignment` and `~.Text.set_verticalalignment`: see below -* AxisLabels' methods (label) +* AxisLabel' methods (label) - - set_axis_direction: "left", "right", "bottom", or "top" - - set_rotation: angle with respect to the reference direction - - set_ha and set_va + - `~.AxisLabel.set_axis_direction`: "left", "right", "bottom", or "top" + - `~.Text.set_rotation`: angle with respect to the reference direction + - `~.Text.set_horizontalalignment` and + `~.Text.set_verticalalignment` Adjusting ticklabels alignment ------------------------------ diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index dd0ed6b583f7..3ba70c3d7d3b 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -55,14 +55,14 @@ axislabel ha right center right center =================== ====== ======== ====== ======== -Ticks are by default direct opposite side of the ticklabels. To make ticks to -the same side of the ticklabels, :: +Tick orientation is controlled by :rc:`xtick.direction` and +:rc:`ytick.direction`; they can be manually adjusted using :: - ax.axis["bottom"].major_ticks.set_tick_out(True) + ax.axis["bottom"].major_ticks.set_tick_direction("in") # or "out", "inout" The following attributes can be customized (use the ``set_xxx`` methods): -* `Ticks`: ticksize, tick_out +* `Ticks`: ticksize, tick_direction * `TickLabels`: pad * `AxisLabel`: pad """ @@ -109,16 +109,16 @@ class Ticks(AttributeCopier, Line2D): Ticks are derived from `.Line2D`, and note that ticks themselves are markers. Thus, you should use set_mec, set_mew, etc. - To change the tick size (length), you need to use - `set_ticksize`. To change the direction of the ticks (ticks are - in opposite direction of ticklabels by default), use - ``set_tick_out(False)`` + To change the tick size (length), use `set_ticksize`. + To change the direction of the ticks, use ``set_tick_direction("in")`` (or + "out", or "inout"). """ + locs_angles_labels = _api.deprecated("3.11")(property(lambda self: [])) + + @_api.delete_parameter("3.11", "tick_out", alternative="tick_direction") def __init__(self, ticksize, tick_out=False, *, axis=None, **kwargs): self._ticksize = ticksize - self.locs_angles_labels = [] - self.set_tick_out(tick_out) self._axis = axis @@ -152,13 +152,33 @@ def get_markeredgecolor(self): def get_markeredgewidth(self): return self.get_attribute_from_ref_artist("markeredgewidth") + def set_tick_direction(self, direction): + _api.check_in_list(["in", "out", "inout"], direction=direction) + self._tick_dir = direction + + def get_tick_direction(self): + return self._tick_dir + def set_tick_out(self, b): - """Set whether ticks are drawn inside or outside the axes.""" - self._tick_out = b + """ + Set whether ticks are drawn inside or outside the axes. + .. admonition:: Discouraged + Consider using the more general method `.set_tick_direction` instead. + """ + self._tick_dir = "out" if b else "in" + + @_api.deprecated("3.11", alternative="get_tick_direction") def get_tick_out(self): """Return whether ticks are drawn inside or outside the axes.""" - return self._tick_out + if self._tick_dir == "in": + return False + elif self._tick_dir == "out": + return True + else: + raise ValueError( + f"Tick direction ({self._tick_dir!r}) not supported by get_tick_out, " + f"use get_tick_direction instead") def set_ticksize(self, ticksize): """Set length of the ticks in points.""" @@ -171,7 +191,11 @@ def get_ticksize(self): def set_locs_angles(self, locs_angles): self.locs_angles = locs_angles - _tickvert_path = Path([[0., 0.], [1., 0.]]) + _tick_paths = { + "in": Path([[0, 0], [+1, 0]]), + "inout": Path([[-1/2, 0.], [+1/2, 0]]), + "out": Path([[-1, 0], [0, 0]]), + } def draw(self, renderer): if not self.get_visible(): @@ -183,18 +207,17 @@ def draw(self, renderer): gc.set_linewidth(self.get_markeredgewidth()) gc.set_alpha(self._alpha) + tickvert_path = self._tick_paths[self._tick_dir] path_trans = self.get_transform() marker_transform = (Affine2D() .scale(renderer.points_to_pixels(self._ticksize))) - if self.get_tick_out(): - marker_transform.rotate_deg(180) for loc, angle in self.locs_angles: locs = path_trans.transform_non_affine(np.array([loc])) if self.axes and not self.axes.viewLim.contains(*locs[0]): continue renderer.draw_markers( - gc, self._tickvert_path, + gc, tickvert_path, marker_transform + Affine2D().rotate_deg(angle), Path(locs), path_trans.get_affine()) @@ -208,8 +231,9 @@ class LabelBase(mtext.Text): text_ref_angle, and offset_radius attributes. """ + locs_angles_labels = _api.deprecated("3.11")(property(lambda self: [])) + def __init__(self, *args, **kwargs): - self.locs_angles_labels = [] self._ref_angle = 0 self._offset_radius = 0. @@ -867,14 +891,16 @@ def _init_ticks(self, **kwargs): + self.offset_transform) self.major_ticks = Ticks( - kwargs.get( + ticksize=kwargs.get( "major_tick_size", mpl.rcParams[f"{axis_name}tick.major.size"]), + tick_direction=mpl.rcParams[f"{axis_name}tick.direction"], axis=self.axis, transform=trans) self.minor_ticks = Ticks( - kwargs.get( + ticksize=kwargs.get( "minor_tick_size", mpl.rcParams[f"{axis_name}tick.minor.size"]), + tick_direction=mpl.rcParams[f"{axis_name}tick.direction"], axis=self.axis, transform=trans) size = mpl.rcParams[f"{axis_name}tick.labelsize"] @@ -926,14 +952,13 @@ def _update_ticks(self, renderer=None): if renderer is None: renderer = self.get_figure(root=True)._get_renderer() - dpi_cor = renderer.points_to_pixels(1.) - if self.major_ticks.get_visible() and self.major_ticks.get_tick_out(): - ticklabel_pad = self.major_ticks._ticksize * dpi_cor - self.major_ticklabels._external_pad = ticklabel_pad - self.minor_ticklabels._external_pad = ticklabel_pad - else: - self.major_ticklabels._external_pad = 0 - self.minor_ticklabels._external_pad = 0 + self.major_ticklabels._external_pad = \ + self.minor_ticklabels._external_pad = ( + renderer.points_to_pixels(self.major_ticks._ticksize) + * {"in": 0, "inout": 1/2, "out": 1}[ + self.major_ticks.get_tick_direction()] + * self.major_ticks.get_visible() # 0 if invisible. + ) majortick_iter, minortick_iter = \ self._axis_artist_helper.get_tick_iterators(self.axes) @@ -1008,13 +1033,18 @@ def _update_label(self, renderer): return if self._ticklabel_add_angle != self._axislabel_add_angle: - if ((self.major_ticks.get_visible() - and not self.major_ticks.get_tick_out()) - or (self.minor_ticks.get_visible() - and not self.major_ticks.get_tick_out())): - axislabel_pad = self.major_ticks._ticksize - else: - axislabel_pad = 0 + axislabel_pad = max( + # major pad: + self.major_ticks._ticksize + * {"in": 1, "inout": 1/2, "out": 0}[ + self.major_ticks.get_tick_direction()] + * self.major_ticks.get_visible(), # 0 if invisible. + # minor pad: + self.minor_ticks._ticksize + * {"in": 1, "inout": 1/2, "out": 0}[ + self.minor_ticks.get_tick_direction()] + * self.minor_ticks.get_visible(), # 0 if invisible. + ) else: axislabel_pad = max(self.major_ticklabels._axislabel_pad, self.minor_ticklabels._axislabel_pad) diff --git a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index 96d8a2cde0f3..f366b9e96537 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -19,7 +19,7 @@ def test_ticks(): ticks_in.set_locs_angles(locs_angles) ax.add_artist(ticks_in) - ticks_out = Ticks(ticksize=10, tick_out=True, color='C3', axis=ax.xaxis) + ticks_out = Ticks(ticksize=10, tick_direction="out", color='C3', axis=ax.xaxis) ticks_out.set_locs_angles(locs_angles) ax.add_artist(ticks_out) @@ -92,11 +92,11 @@ def test_axis_artist(): for loc in ('left', 'right', 'bottom'): helper = AxisArtistHelperRectlinear.Fixed(ax, loc=loc) axisline = AxisArtist(ax, helper, offset=None, axis_direction=loc) + axisline.major_ticks.set_tick_direction("in") ax.add_artist(axisline) # Settings for bottom AxisArtist. axisline.set_label("TTT") - axisline.major_ticks.set_tick_out(False) axisline.label.set_pad(5) ax.set_ylabel("Test") diff --git a/lib/mpl_toolkits/axisartist/tests/test_axislines.py b/lib/mpl_toolkits/axisartist/tests/test_axislines.py index a47ab2ea8a31..10c50c271ef2 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axislines.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axislines.py @@ -11,7 +11,8 @@ @image_comparison(['SubplotZero.png'], style='default', tol=0.02) def test_SubplotZero(): # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({ + "text.kerning_factor": 6, "xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure() @@ -33,7 +34,8 @@ def test_SubplotZero(): @image_comparison(['Subplot.png'], style='default', tol=0.02) def test_Subplot(): # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({ + "text.kerning_factor": 6, "xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure() @@ -62,6 +64,8 @@ def test_Axes(): @image_comparison(['ParasiteAxesAuxTrans_meshplot.png'], remove_text=True, style='default', tol=0.075) def test_ParasiteAxesAuxTrans(): + # Remove this line when this test image is regenerated. + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) data = np.ones((6, 6)) data[2, 2] = 2 data[0, :] = 0 @@ -93,6 +97,8 @@ def test_ParasiteAxesAuxTrans(): @image_comparison(['axisline_style.png'], remove_text=True, style='mpl20') def test_axisline_style(): + # Remove this line when this test image is regenerated. + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(2, 2)) ax = fig.add_subplot(axes_class=AxesZero) ax.axis["xzero"].set_axisline_style("-|>") @@ -107,6 +113,8 @@ def test_axisline_style(): @image_comparison(['axisline_style_size_color.png'], remove_text=True, style='mpl20') def test_axisline_style_size_color(): + # Remove this line when this test image is regenerated. + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(2, 2)) ax = fig.add_subplot(axes_class=AxesZero) ax.axis["xzero"].set_axisline_style("-|>", size=2.0, facecolor='r') @@ -121,6 +129,8 @@ def test_axisline_style_size_color(): @image_comparison(['axisline_style_tight.png'], remove_text=True, style='mpl20') def test_axisline_style_tight(): + # Remove this line when this test image is regenerated. + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(2, 2), layout='tight') ax = fig.add_subplot(axes_class=AxesZero) ax.axis["xzero"].set_axisline_style("-|>", size=5, facecolor='g') @@ -135,6 +145,8 @@ def test_axisline_style_tight(): # TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['subplotzero_ylabel.png'], style='mpl20', tol=0.02) def test_subplotzero_ylabel(): + # Remove this line when this test image is regenerated. + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure() ax = fig.add_subplot(111, axes_class=SubplotZero) diff --git a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py index feb667af013e..98d49dc0cf37 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py +++ b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py @@ -23,6 +23,8 @@ def test_subplot(): # remove when image is regenerated. @image_comparison(['curvelinear3.png'], style='default', tol=5) def test_curvelinear3(): + # Remove this line when this test image is regenerated. + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(5, 5)) tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) + @@ -70,7 +72,8 @@ def test_curvelinear3(): @image_comparison(['curvelinear4.png'], style='default', tol=0.9) def test_curvelinear4(): # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({ + "text.kerning_factor": 6, "xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(5, 5)) diff --git a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py index ac31b8b30c97..e59e31fe3c88 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py @@ -17,6 +17,9 @@ @image_comparison(['custom_transform.png'], style='default', tol=0.2) def test_custom_transform(): + # Remove this line when this test image is regenerated. + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) + class MyTransform(Transform): input_dims = output_dims = 2 @@ -79,6 +82,8 @@ def inverted(self): # TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['polar_box.png'], style='default', tol=0.09) def test_polar_box(): + # Remove this line when this test image is regenerated. + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(5, 5)) # PolarAxes.PolarTransform takes radian. However, we want our coordinate @@ -139,7 +144,9 @@ def test_polar_box(): # Remove tol & kerning_factor when this test image is regenerated. @image_comparison(['axis_direction.png'], style='default', tol=0.15) def test_axis_direction(): - plt.rcParams['text.kerning_factor'] = 6 + # Remove this line when this test image is regenerated. + plt.rcParams.update({ + "text.kerning_factor": 6, "xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(5, 5))