diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 37cc3d5f89e8..f155af8cc2c1 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -502,6 +502,125 @@ def formatter(self, formatter): self._formatter = formatter +class _TickCollection: + """ + A facade for tick lists based on _LazyTickList. + + This provides an interface for manipulating the ticks collectively. It + removes the need to address individual elements of the tick lists and thus + opens up a path of replacing individual lines with collections while + keeping the API stable:: + + - tick lists - tick collection + - tick (Tick) - tick1lines (LineCollection) + - tick1line (Line2D) - tick2lines (LineCollection) + - tick2line (Line2D) ===> - tick1labels (TextCollection*) + - tick1label (Text) - tick2labels (TextCollection*) + - tick2label (Text) - gridline + - gridline (Line2D) + - tick (Tick) + - ... + - tick (Tick) + - ... + + (*) TextCollection does not yet exists. Probably worth implementing, but + we can also use lists of Text for the time being. + + """ + def __init__(self, axis, ticklist_name): + """ + We cannot initialize this in Axis using + ``_TickCollection(self.majorTicks)``, because that would trigger the + evaluation mechanism of the _LazyTickList. Therefore we delay the + access to the latest possible point via the property + ``self._ticklist``. + """ + self._axis = axis + self._ticklist_name = ticklist_name + + def __len__(self): + return len(self._ticklist) + + @property + def _ticklist(self): + """Delayed access to resolve _LazyTickList as late as possible.""" + return getattr(self._axis, self._ticklist_name) + + def apply_params(self, **kwargs): + """Apply **kwargs to all ticks.""" + for tick in self._ticklist: + tick._apply_params(**kwargs) + + def set_clip_path(self, clippath, transform): + """ + Set the clip path for all ticks. + + See `.Artist.set_clip_path`. + """ + for tick in self._ticklist: + tick.set_clip_path(clippath, transform) + + def _get_tick_position(self): + """See Axis._get_ticks_position().""" + tick = self._ticklist[0] + if (tick.tick1line.get_visible() + and not tick.tick2line.get_visible() + and tick.label1.get_visible() + and not tick.label2.get_visible()): + return 1 + elif (tick.tick2line.get_visible() + and not tick.tick1line.get_visible() + and tick.label2.get_visible() + and not tick.label1.get_visible()): + return 2 + elif (tick.tick1line.get_visible() + and tick.tick2line.get_visible() + and tick.label1.get_visible() + and not tick.label2.get_visible()): + return "default" + else: + return "unknown" + + def get_ticks(self, numticks, tick_getter): + #if numticks is None: + # numticks = len(self.get_majorticklocs()) + + while len(self._ticklist) < numticks: + # Update the new tick label properties from the old. + tick = tick_getter() + self._ticklist.append(tick) + self._axis._copy_tick_props(self._ticklist[0], tick) + + return self._ticklist[:numticks] + + def get_tick_padding(self): + if not self._ticklist: + return 0 + else: + return self._ticklist[0].get_tick_padding() + + def get_pad_pixels(self): + return self._ticklist[0].get_pad_pixels() + + def set_label_alignment(self, label1_va, label1_ha, label2_va, label2_ha): + for t in self._ticklist: + t.label1.set_va(label1_va) + t.label1.set_ha(label1_ha) + t.label2.set_va(label2_va) + t.label2.set_ha(label2_ha) + + def get_grid_visible(self): + # Return True/False if all grid lines are on or off, None if they are + # not all in the same state. + if all(tick.gridline.get_visible() for tick in self._ticklist): + return True + elif not any(tick.gridline.get_visible() for tick in self._ticklist): + return False + else: + return None + + + class _LazyTickList: """ A descriptor for lazy instantiation of tick lists. @@ -639,6 +758,9 @@ def __init__(self, axes, *, pickradius=15, clear=True): self._major_tick_kw = dict() self._minor_tick_kw = dict() + self._major_ticks = _TickCollection(self, "majorTicks") + self._minor_ticks = _TickCollection(self, "minorTicks") + if clear: self.clear() else: @@ -695,6 +817,8 @@ def _get_axis_name(self): return next(name for name, axis in self.axes._axis_map.items() if axis is self) + # Interface to address the ticklists via a single entity + # During initialization, Axis objects often create ticks that are later # unused; this turns out to be a very slow step. Instead, use a custom # descriptor to make the tick lists lazy and instantiate them as needed. @@ -965,12 +1089,10 @@ def set_tick_params(self, which='major', reset=False, **kwargs): else: if which in ['major', 'both']: self._major_tick_kw.update(kwtrans) - for tick in self.majorTicks: - tick._apply_params(**kwtrans) + self._major_ticks.apply_params(**kwtrans) if which in ['minor', 'both']: self._minor_tick_kw.update(kwtrans) - for tick in self.minorTicks: - tick._apply_params(**kwtrans) + self._minor_ticks.apply_params(**kwtrans) # labelOn and labelcolor also apply to the offset text. if 'label1On' in kwtrans or 'label2On' in kwtrans: self.offsetText.set_visible( @@ -1107,8 +1229,8 @@ def _translate_tick_params(cls, kw, reverse=False): def set_clip_path(self, path, transform=None): super().set_clip_path(path, transform) - for child in self.majorTicks + self.minorTicks: - child.set_clip_path(path, transform) + self._major_ticks.set_clip_path(path, transform) + self._minor_ticks.set_clip_path(path, transform) self.stale = True def get_view_interval(self): @@ -1379,12 +1501,11 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): return None def get_tick_padding(self): - values = [] - if len(self.majorTicks): - values.append(self.majorTicks[0].get_tick_padding()) - if len(self.minorTicks): - values.append(self.minorTicks[0].get_tick_padding()) - return max(values, default=0) + pads = [ + self._major_ticks.get_tick_padding(), + self._minor_ticks.get_tick_padding(), + ] + return max((p for p in pads if p is not None), default=0) @martist.allow_rasterization def draw(self, renderer): @@ -1651,14 +1772,8 @@ def get_major_ticks(self, numticks=None): """ if numticks is None: numticks = len(self.get_majorticklocs()) - - while len(self.majorTicks) < numticks: - # Update the new tick label properties from the old. - tick = self._get_tick(major=True) - self.majorTicks.append(tick) - self._copy_tick_props(self.majorTicks[0], tick) - - return self.majorTicks[:numticks] + return self._major_ticks.get_ticks( + numticks, tick_getter=functools.partial(self._get_tick, True)) def get_minor_ticks(self, numticks=None): r""" @@ -1677,14 +1792,8 @@ def get_minor_ticks(self, numticks=None): """ if numticks is None: numticks = len(self.get_minorticklocs()) - - while len(self.minorTicks) < numticks: - # Update the new tick label properties from the old. - tick = self._get_tick(major=False) - self.minorTicks.append(tick) - self._copy_tick_props(self.minorTicks[0], tick) - - return self.minorTicks[:numticks] + return self._minor_ticks.get_ticks( + numticks, tick_getter=functools.partial(self._get_tick, False)) def grid(self, visible=None, which='major', **kwargs): """ @@ -2286,26 +2395,11 @@ def _get_ticks_position(self): - "default" if only tick1line, tick2line and label1 are visible; - "unknown" otherwise. """ - major = self.majorTicks[0] - minor = self.minorTicks[0] - if all(tick.tick1line.get_visible() - and not tick.tick2line.get_visible() - and tick.label1.get_visible() - and not tick.label2.get_visible() - for tick in [major, minor]): - return 1 - elif all(tick.tick2line.get_visible() - and not tick.tick1line.get_visible() - and tick.label2.get_visible() - and not tick.label1.get_visible() - for tick in [major, minor]): - return 2 - elif all(tick.tick1line.get_visible() - and tick.tick2line.get_visible() - and tick.label1.get_visible() - and not tick.label2.get_visible() - for tick in [major, minor]): - return "default" + major_pos = self._major_ticks._get_tick_position() + minor_pos = self._minor_ticks._get_tick_position() + + if major_pos == minor_pos: + return major_pos else: return "unknown" diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index f78c59fa50ea..6e622979ff10 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2438,14 +2438,7 @@ def key_press_handler(event, canvas=None, toolbar=None): if event.inaxes is None: return - # these bindings require the mouse to be over an Axes to trigger - def _get_uniform_gridstate(ticks): - # Return True/False if all grid lines are on or off, None if they are - # not all in the same state. - return (True if all(tick.gridline.get_visible() for tick in ticks) else - False if not any(tick.gridline.get_visible() for tick in ticks) else - None) - + # these bindings require the mouse to be over an axes to trigger ax = event.inaxes # toggle major grids in current Axes (default key 'g') # Both here and below (for 'G'), we do nothing if *any* grid (major or @@ -2453,10 +2446,10 @@ def _get_uniform_gridstate(ticks): # customization. if (event.key in rcParams['keymap.grid'] # Exclude minor grids not in a uniform state. - and None not in [_get_uniform_gridstate(ax.xaxis.minorTicks), - _get_uniform_gridstate(ax.yaxis.minorTicks)]): - x_state = _get_uniform_gridstate(ax.xaxis.majorTicks) - y_state = _get_uniform_gridstate(ax.yaxis.majorTicks) + and None not in [ax.xaxis._minor_ticks.get_grid_visible(), + ax.yaxis._minor_ticks.get_grid_visible()]): + x_state = ax.xaxis._major_ticks.get_grid_visible() + y_state = ax.yaxis._major_ticks.get_grid_visible() cycle = [(False, False), (True, False), (True, True), (False, True)] try: x_state, y_state = ( @@ -2472,10 +2465,10 @@ def _get_uniform_gridstate(ticks): # toggle major and minor grids in current Axes (default key 'G') if (event.key in rcParams['keymap.grid_minor'] # Exclude major grids not in a uniform state. - and None not in [_get_uniform_gridstate(ax.xaxis.majorTicks), - _get_uniform_gridstate(ax.yaxis.majorTicks)]): - x_state = _get_uniform_gridstate(ax.xaxis.minorTicks) - y_state = _get_uniform_gridstate(ax.yaxis.minorTicks) + and None not in [ax.xaxis._major_ticks.get_grid_visible(), + ax.yaxis._major_ticks.get_grid_visible()]): + x_state = ax.xaxis._minor_ticks.get_grid_visible() + y_state = ax.yaxis._minor_ticks.get_grid_visible() cycle = [(False, False), (True, False), (True, True), (False, True)] try: x_state, y_state = ( diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 7fe6045039b1..0c3f1339d145 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -1528,11 +1528,11 @@ def drag_pan(self, button, key, x, y): trans, vert1, horiz1 = self.get_yaxis_text1_transform(0.0) trans, vert2, horiz2 = self.get_yaxis_text2_transform(0.0) - for t in self.yaxis.majorTicks + self.yaxis.minorTicks: - t.label1.set_va(vert1) - t.label1.set_ha(horiz1) - t.label2.set_va(vert2) - t.label2.set_ha(horiz2) + for tick_collection in [ + self.yaxis._major_ticks, self.yaxis._minor_ticks]: + tick_collection.set_label_alignments( + label1_va=vert1, label1_ha=horiz1, + label2_va=vert2, label2_ha=horiz2) elif p.mode == 'zoom': (startt, startr), (t, r) = p.trans_inverse.transform(