Skip to content

Commit c54a2e0

Browse files
timhoffmy9c
andcommitted
Add Spines class as a container for all Axes spines
Co-authored-by: yech1990 <yech1990@gmail.com>
1 parent 065769b commit c54a2e0

File tree

2 files changed

+91
-1
lines changed

2 files changed

+91
-1
lines changed

lib/matplotlib/axes/_base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def __init__(self, fig, rect,
494494
self.set_box_aspect(box_aspect)
495495
self._axes_locator = None # Optionally set via update(kwargs).
496496

497-
self.spines = self._gen_axes_spines()
497+
self.spines = mspines.Spines.from_dict(self._gen_axes_spines())
498498

499499
# this call may differ for non-sep axes, e.g., polar
500500
self._init_axis()

lib/matplotlib/spines.py

+90
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from collections.abc import MutableMapping
2+
import functools
3+
14
import numpy as np
25

36
import matplotlib
@@ -534,3 +537,90 @@ def set_color(self, c):
534537
"""
535538
self.set_edgecolor(c)
536539
self.stale = True
540+
541+
542+
class SpinesProxy:
543+
"""
544+
A proxy to broadcast ``set_*`` method calls to all contained `.Spines`.
545+
546+
The supported methods are determined dynamically based on the contained
547+
spines. If not all spines support a given method, it's executed only on
548+
the subset of spines that support it.
549+
"""
550+
def __init__(self, spine_dict):
551+
self._spine_dict = spine_dict
552+
553+
def __getattr__(self, name):
554+
broadcast_targets = [spine for spine in self._spine_dict.values()
555+
if hasattr(spine, name)]
556+
if not name.startswith('set_') or not broadcast_targets:
557+
raise ValueError(f"'SpinesProxy' object has no attribute '{name}'")
558+
559+
def x(_targets, _funcname, *args, **kwargs):
560+
for spine in _targets:
561+
getattr(spine, _funcname)(*args, **kwargs)
562+
x = functools.partial(x, broadcast_targets, name)
563+
x.__doc__ = broadcast_targets[0].__doc__
564+
return x
565+
566+
def __dir__(self):
567+
names = []
568+
for spine in self._spine_dict.values():
569+
names.extend(name
570+
for name in dir(spine) if name.startswith('set_'))
571+
return list(sorted(set(names)))
572+
573+
574+
class Spines(MutableMapping):
575+
r"""
576+
The container of all `.Spine`\s in an Axes.
577+
578+
Individual spines can be accessed via attribute access of the spine names::
579+
580+
spines.top.set_visible(False)
581+
582+
For backward compatibility, it's also possible to use item access::
583+
584+
spines['top'].set_visible(False)
585+
586+
Setting spine properties can be broadcasted to all spines using the proxy
587+
``spines.all``:
588+
589+
spines.all.set_visible(False)
590+
591+
Attributes
592+
----------
593+
all : SpinesProxy
594+
A proxy to broadcast ``set_*`` method calls to all contained `.Spines`.
595+
596+
"""
597+
def __init__(self, **kwargs):
598+
self._dict = kwargs
599+
self.all = SpinesProxy(self._dict)
600+
601+
@classmethod
602+
def from_dict(cls, d):
603+
return cls(**d)
604+
605+
def __getstate__(self):
606+
return self._dict
607+
608+
def __setstate__(self, state):
609+
self.__init__(**state)
610+
611+
def __getitem__(self, key):
612+
return self._dict[key]
613+
614+
def __setitem__(self, key, value):
615+
# TODO: Do we want to deprecate adding spines?
616+
self._dict[key] = value
617+
618+
def __delitem__(self, key):
619+
# TODO: Do we want to deprecate deleting spines?
620+
del self._dict[key]
621+
622+
def __iter__(self):
623+
return iter(self._dict)
624+
625+
def __len__(self):
626+
return len(self._dict)

0 commit comments

Comments
 (0)