diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index 08bb73b08e11..8219b78c6a88 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -55,14 +55,20 @@ 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, :: +Direction of ticks follows the setting in rcParams (default is "out"). To +change it, :: - ax.axis["bottom"].major_ticks.set_tick_out(True) + ax.axis["bottom"].major_ticks.set_tickdir("in") + +Ticks can be oriented either normal to the axisline or parallel to the grid +lines. The default is "normal" if "tickdir" is "out", "parallel" otherwise. +To change it, :: + + ax.axis["bottom"].major_ticks.set_tick_orientation("normal") The following attributes can be customized (use the ``set_xxx`` methods): -* `Ticks`: ticksize, tick_out +* `Ticks`: ticksize, tickdir * `TickLabels`: pad * `AxisLabel`: pad """ @@ -72,6 +78,7 @@ from operator import methodcaller +import warnings import numpy as np @@ -109,17 +116,37 @@ 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_tickdir ("out" corresponds to the side of + the label, "in" to the opposite side). """ - def __init__(self, ticksize, tick_out=False, *, axis=None, **kwargs): + # tick_out is mostly deprecated in favor of tickdir. + + @_api.delete_parameter("3.7", "tick_out", alternative="tickdir") + def __init__(self, ticksize, tick_out=None, + *, tick_orientation="auto", + axis=None, **kwargs): self._ticksize = ticksize self.locs_angles_labels = [] - self.set_tick_out(tick_out) + if "tickdir" in kwargs: + if tick_out is not None: + raise ValueError("tickdir and tick_out" + "cannot be used together") + self.set_tickdir(kwargs.pop("tickdir")) + else: + # The default value for tick_out was False. We changed it to None + # to catch whether it explicily set by the user. "None" may not be + # a good choice of value though. + if tick_out is None: + warnings.warn("The dwfault behavior will change. " + "Explicitly set tickdir parameter if you want.") + tick_out = False + + self.set_tickdir({True: "out", False: "in"}[bool(tick_out)]) + + self.set_tick_orientation(tick_orientation) self._axis = axis if self._axis is not None: @@ -152,13 +179,61 @@ def get_markeredgecolor(self): def get_markeredgewidth(self): return self.get_attribute_from_ref_artist("markeredgewidth") + def set_tickdir(self, tickdir): + _api.check_in_list(self._tick_paths, tickdir=tickdir) + self._tickdir = tickdir + + def get_tickdir(self): + return self._tickdir + + def set_tick_orientation(self, mode): + """ + Set how tick orientation will be determined. + + Parameters + ---------- + mode : {"parallel", "normal", "auto"} + 'parallel' - ticks along the grid lines + 'normal' - ticks normal to axis line. + 'auto' - 'normal' if tickdir is 'out' else 'parallel' + """ + _api.check_in_list(["auto", "normal", "parallel"], + tick_orientation=mode) + self._tick_orientation = mode + + def get_tick_orientation(self, interpret_auto=True): + """ + Return orientation of ticks. + + Parameters + ---------- + interpret_auto : bool, default if True + If True and tick_orientation is 'auto', return the + interpreted value ('normal' if dicktir is out, else 'parallel). + If False, return 'auto' as 'auto'. + + Returns + ------- + tick_orientation : {"parallel", "normal", "auto"} + """ + tick_orientation = self._tick_orientation + if tick_orientation == "auto" and interpret_auto: + tick_orientation = ("normal" if self._tickdir == "out" + else "parallel") + + return tick_orientation + def set_tick_out(self, b): """Set whether ticks are drawn inside or outside the axes.""" - self._tick_out = b + self.set_tickdir({True: "out", False: "in"}[bool(b)]) def get_tick_out(self): """Return whether ticks are drawn inside or outside the axes.""" - return self._tick_out + if self._tickdir == "out": + return True + elif self._tickdir == "in": + return False + raise ValueError(f"tickdir is {self._tickdir}") def set_ticksize(self, ticksize): """Set length of the ticks in points.""" @@ -171,7 +246,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 = { + "out": Path([[0, 0], [-1, 0]]), + "in": Path([[0, 0], [1, 0]]), + "inout": Path([[-1/2, 0], [1/2, 0]]), + } def draw(self, renderer): if not self.get_visible(): @@ -185,15 +264,14 @@ def draw(self, renderer): 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) + tick_path = self._tick_paths[self._tickdir] 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, tick_path, marker_transform + Affine2D().rotate_deg(angle), Path(locs), path_trans.get_affine()) @@ -868,12 +946,14 @@ def _init_ticks(self, **kwargs): kwargs.get( "major_tick_size", mpl.rcParams[f"{axis_name}tick.major.size"]), - axis=self.axis, transform=trans) + axis=self.axis, transform=trans, + tickdir=mpl.rcParams[f"{axis_name}tick.direction"]) self.minor_ticks = Ticks( kwargs.get( "minor_tick_size", mpl.rcParams[f"{axis_name}tick.minor.size"]), - axis=self.axis, transform=trans) + axis=self.axis, transform=trans, + tickdir=mpl.rcParams[f"{axis_name}tick.direction"]) size = mpl.rcParams[f"{axis_name}tick.labelsize"] self.major_ticklabels = TickLabels( @@ -895,7 +975,7 @@ def _init_ticks(self, **kwargs): "minor_tick_pad", mpl.rcParams[f"{axis_name}tick.minor.pad"]), ) - def _get_tick_info(self, tick_iter): + def _get_tick_info(self, tick_iter, tick_orientation="parallel"): """ Return a pair of: @@ -909,9 +989,16 @@ def _get_tick_info(self, tick_iter): for loc, angle_normal, angle_tangent, label in tick_iter: angle_label = angle_tangent - 90 + ticklabel_add_angle - angle_tick = (angle_normal - if 90 <= (angle_label - angle_normal) % 360 <= 270 - else angle_normal + 180) + if tick_orientation == "parallel": # tick along the gridlines + angle_tick = ( + angle_normal + if 90 <= (angle_label - angle_normal) % 360 <= 270 + else angle_normal + 180) + elif tick_orientation == "normal": # tick normal to axisline. + angle_tick = 180+angle_label + else: + raise ValueError( + f"Unsupported tick_orientation of {tick_orientation}") ticks_loc_angle.append([loc, angle_tick]) ticklabels_loc_angle_label.append([loc, angle_label, label]) @@ -925,24 +1012,31 @@ def _update_ticks(self, renderer=None): renderer = self.figure._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 + multiplier = ( + self.major_ticks.get_visible() + * {"out": 1, "inout": .5, "in": 0}[self.major_ticks._tickdir]) + + self.major_ticklabels._external_pad = \ + multiplier * self.major_ticks._ticksize * dpi_cor + self.minor_ticklabels._external_pad = \ + multiplier * self.major_ticks._ticksize * dpi_cor majortick_iter, minortick_iter = \ self._axis_artist_helper.get_tick_iterators(self.axes) + tick_orientation = self.major_ticks.get_tick_orientation( + interpret_auto=True) tick_loc_angle, ticklabel_loc_angle_label = \ - self._get_tick_info(majortick_iter) + self._get_tick_info(majortick_iter, tick_orientation) + self.major_ticks.set_locs_angles(tick_loc_angle) self.major_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label) + tick_orientation = self.minor_ticks.get_tick_orientation( + interpret_auto=True) tick_loc_angle, ticklabel_loc_angle_label = \ - self._get_tick_info(minortick_iter) + self._get_tick_info(minortick_iter, tick_orientation) + self.minor_ticks.set_locs_angles(tick_loc_angle) self.minor_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label) @@ -1005,15 +1099,30 @@ def _update_label(self, renderer): if not self.label.get_visible(): return + # We calculate the pad size for the axislabel. 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 + # If ticklabels and axislabel are on different side, we only + # consider the padding for the ticks only. + + # "in", "out" and "inout" are relative to the ticks. Therefore, "in" + # means that axislabel and ticks are on the same side while + # ticklabels are on the other side. + ticksizes = [] + # ticksize of the major_ticks + ticksizes.append( + self.major_ticks.get_visible() + * {"out": 0, "inout": .5, "in": 1}[self.major_ticks._tickdir] + * self.major_ticks._ticksize + ) + ticksizes.append( + self.minor_ticks.get_visible() + * {"out": 0, "inout": .5, "in": 1}[self.minor_ticks._tickdir] + * self.minor_ticks._ticksize + ) + axislabel_pad = max(ticksizes) else: + # If ticklabels and axislabel are on the same side, we use values + # from the the ticklabels. axislabel_pad = max(self.major_ticklabels._axislabel_pad, self.minor_ticklabels._axislabel_pad) diff --git a/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py index 3e4ae747e853..53b8688fbab0 100644 --- a/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py @@ -313,6 +313,10 @@ def new_floating_axis(self, nth_coord, # axisline.major_ticklabels.set_visible(True) # axisline.minor_ticklabels.set_visible(False) + # For floating axis, we force the tick_orientation of "parallel". + axisline.major_ticks.set_tick_orientation("parallel") + axisline.minor_ticks.set_tick_orientation("parallel") + return axisline def _update_grid(self, x1, y1, x2, y2): diff --git a/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_dir.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_dir.png new file mode 100644 index 000000000000..34b2f1fbe3ba Binary files /dev/null and b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_dir.png differ diff --git a/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_tick_orientation.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_tick_orientation.png new file mode 100644 index 000000000000..60643bf08ce4 Binary files /dev/null and b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_tick_orientation.png differ diff --git a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index 391fd116ea86..a5cd963df7fb 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -1,9 +1,16 @@ import matplotlib.pyplot as plt from matplotlib.testing.decorators import image_comparison +import matplotlib.transforms as mtransforms + +from mpl_toolkits import axisartist from mpl_toolkits.axisartist import AxisArtistHelperRectlinear from mpl_toolkits.axisartist.axis_artist import (AxisArtist, AxisLabel, LabelBase, Ticks, TickLabels) +from mpl_toolkits.axisartist.grid_helper_curvelinear import ( + GridHelperCurveLinear) + +from matplotlib._api.deprecation import MatplotlibDeprecationWarning @image_comparison(['axis_artist_ticks.png'], style='default') @@ -15,11 +22,11 @@ def test_ticks(): locs_angles = [((i / 10, 0.0), i * 30) for i in range(-1, 12)] - ticks_in = Ticks(ticksize=10, axis=ax.xaxis) + ticks_in = Ticks(ticksize=10, tickdir="in", axis=ax.xaxis) 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, tickdir="out", color='C3', axis=ax.xaxis) ticks_out.set_locs_angles(locs_angles) ax.add_artist(ticks_out) @@ -53,7 +60,7 @@ def test_ticklabels(): ax.plot([0.2, 0.4], [0.5, 0.5], "o") - ticks = Ticks(ticksize=10, axis=ax.xaxis) + ticks = Ticks(ticksize=10, tickdir="in", axis=ax.xaxis) ax.add_artist(ticks) locs_angles_labels = [((0.2, 0.5), -90, "0.2"), ((0.4, 0.5), -120, "0.4")] @@ -78,8 +85,9 @@ def test_ticklabels(): @image_comparison(['axis_artist.png'], style='default') def test_axis_artist(): - # Remove this line when this test image is regenerated. + # Remove these lines when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig, ax = plt.subplots() @@ -97,3 +105,100 @@ def test_axis_artist(): axisline.label.set_pad(5) ax.set_ylabel("Test") + + +def test_tickout_kwargs(recwarn): + """ + test that 'singular' versions of LineCollection props raise an + MatplotlibDeprecationWarning rather than overriding the 'plural' versions + (e.g., to prevent 'color' from overriding 'colors', see issue #4297) + """ + + Ticks(1) + Ticks(1, False) + Ticks(1, False) + Ticks(1, tick_out=True) + Ticks(1, tick_out=False) + + assert issubclass(recwarn[0].category, UserWarning) + # May need to check the message + + assert all(issubclass(wi.category, MatplotlibDeprecationWarning) + for wi in recwarn[1:]) + + +def _setup_axes_for_axis_artist_dir(fig, pos): + ax = fig.add_subplot(pos, axes_class=axisartist.Axes) + + ax.set_ylim(-0.1, 1.5) + ax.set_yticks([0, 1]) + + ax.axis[:].set_visible(False) + + ax.axis["x"] = ax.new_floating_axis(1, 0.5) + + return ax + + +@image_comparison(['axis_artist_dir.png'], style='default') +def test_axis_artist_dir(): + plt.rcParams['text.kerning_factor'] = 6 + + fig = plt.figure(figsize=(4, 5), num=1) + fig.clf() + + from matplotlib.gridspec import GridSpec + gs = GridSpec(3, 4, figure=fig) + gsi = iter(gs) + + for td in ["in", "out", "inout"]: + for ld in "+-": + for tld in "+-": + ax = _setup_axes_for_axis_artist_dir(fig, next(gsi)) + axis = ax.axis["x"] + axis.major_ticks.set_ticksize(8) + axis.label.set_text("Label") + axis.toggle(ticklabels=True) + axis.set_axislabel_direction(ld) + axis.set_ticklabel_direction(tld) + axis.major_ticks.set_tickdir(td) + + +def _setup_axes_tick_orientation(fig, pos): + tr = mtransforms.Affine2D().skew_deg(0, 30) + + grid_helper = GridHelperCurveLinear(tr) + + ax = fig.add_subplot(pos, axes_class=axisartist.Axes, + grid_helper=grid_helper) + + ax.set_xlim(0, 0.2) + + ax.axis[:].set_visible(False) + + ax.axis["test"] = ax.new_fixed_axis("left") + ax.axis["test"].toggle(ticklabels=False) + + ax.set_aspect(1) + ax.grid(True, axis="y", color="0.8") + return ax + + +@image_comparison(['axis_artist_tick_orientation.png'], style='default') +def test_axis_artist_tick_orientation(): + plt.rcParams['text.kerning_factor'] = 6 + + fig = plt.figure(figsize=(4, 5)) + + from matplotlib.gridspec import GridSpec + gs = GridSpec(3, 3, figure=fig) + gsi = iter(gs) + + for td in ["in", "out", "inout"]: + for to in ["auto", "normal", "parallel"]: + ax = _setup_axes_tick_orientation(fig, next(gsi)) + axis = ax.axis["test"] + axis.major_ticks.set_ticksize(8) + axis.major_ticks.set_tickdir(td) + axis.major_ticks.set_tick_orientation(to) + axis.label.set_text(f"{td}-{to}") diff --git a/lib/mpl_toolkits/axisartist/tests/test_axislines.py b/lib/mpl_toolkits/axisartist/tests/test_axislines.py index 7743cb35aa3b..616c76fe0226 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axislines.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axislines.py @@ -9,8 +9,9 @@ @image_comparison(['SubplotZero.png'], style='default') def test_SubplotZero(): - # Remove this line when this test image is regenerated. + # Remove these lines when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure() @@ -30,8 +31,9 @@ def test_SubplotZero(): @image_comparison(['Subplot.png'], style='default') def test_Subplot(): - # Remove this line when this test image is regenerated. + # Remove these lines when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure() @@ -60,8 +62,9 @@ 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. + # Remove these lines when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) data = np.ones((6, 6)) data[2, 2] = 2 diff --git a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py index d489f492d4d3..000acf5a30d0 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py +++ b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py @@ -21,6 +21,9 @@ def test_subplot(): # remove when image is regenerated. @image_comparison(['curvelinear3.png'], style='default', tol=5) def test_curvelinear3(): + # Remove this lines 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) + @@ -67,8 +70,9 @@ def test_curvelinear3(): # remove when image is regenerated. @image_comparison(['curvelinear4.png'], style='default', tol=0.9) def test_curvelinear4(): - # Remove this line when this test image is regenerated. + # Remove these lines when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({"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 ffc5f6c1b791..42a64c0db91a 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py @@ -16,6 +16,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 @@ -77,8 +80,9 @@ def inverted(self): @image_comparison(['polar_box.png'], style='default', tol=0.04) def test_polar_box(): - # Remove this line when this test image is regenerated. + # Remove these lines when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(5, 5)) @@ -139,8 +143,9 @@ def test_polar_box(): @image_comparison(['axis_direction.png'], style='default', tol=0.071) def test_axis_direction(): - # Remove this line when this test image is regenerated. + # Remove these lines when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(5, 5))