|
| 1 | +from collections.abc import MutableMapping |
| 2 | +import functools |
| 3 | + |
1 | 4 | import numpy as np
|
2 | 5 |
|
3 | 6 | import matplotlib
|
@@ -534,3 +537,114 @@ def set_color(self, c):
|
534 | 537 | """
|
535 | 538 | self.set_edgecolor(c)
|
536 | 539 | self.stale = True
|
| 540 | + |
| 541 | + |
| 542 | +class SpinesProxy: |
| 543 | + """ |
| 544 | + A proxy to broadcast ``set_*`` method calls to all contained `.Spines`. |
| 545 | +
|
| 546 | + The proxy cannot be used for any other operations on its members. |
| 547 | +
|
| 548 | + The supported methods are determined dynamically based on the contained |
| 549 | + spines. If not all spines support a given method, it's executed only on |
| 550 | + the subset of spines that support it. |
| 551 | + """ |
| 552 | + def __init__(self, spine_dict): |
| 553 | + self._spine_dict = spine_dict |
| 554 | + |
| 555 | + def __getattr__(self, name): |
| 556 | + broadcast_targets = [spine for spine in self._spine_dict.values() |
| 557 | + if hasattr(spine, name)] |
| 558 | + if not name.startswith('set_') or not broadcast_targets: |
| 559 | + raise AttributeError( |
| 560 | + f"'SpinesProxy' object has no attribute '{name}'") |
| 561 | + |
| 562 | + def x(_targets, _funcname, *args, **kwargs): |
| 563 | + for spine in _targets: |
| 564 | + getattr(spine, _funcname)(*args, **kwargs) |
| 565 | + x = functools.partial(x, broadcast_targets, name) |
| 566 | + x.__doc__ = broadcast_targets[0].__doc__ |
| 567 | + return x |
| 568 | + |
| 569 | + def __dir__(self): |
| 570 | + names = [] |
| 571 | + for spine in self._spine_dict.values(): |
| 572 | + names.extend(name |
| 573 | + for name in dir(spine) if name.startswith('set_')) |
| 574 | + return list(sorted(set(names))) |
| 575 | + |
| 576 | + |
| 577 | +class Spines(MutableMapping): |
| 578 | + r""" |
| 579 | + The container of all `.Spine`\s in an Axes. |
| 580 | +
|
| 581 | + The interface is dict-like mapping names (e.g. 'left') to `.Spine` objects. |
| 582 | + Additionally it implements some pandas.Series-like features like accessing |
| 583 | + elements by attribute:: |
| 584 | +
|
| 585 | + spines['top'].set_visible(False) |
| 586 | + spines.top.set_visible(False) |
| 587 | +
|
| 588 | + Multiple spines can be addressed simultaneously by passing a list. This |
| 589 | + will return a `SpinesProxy` that broadcasts all ``set_*`` calls to it's |
| 590 | + members:: |
| 591 | +
|
| 592 | + spines[['top', 'right']].set_visible(False) |
| 593 | +
|
| 594 | + Use an open slice to address all spines:: |
| 595 | +
|
| 596 | + spines[:].set_visible(False) |
| 597 | +
|
| 598 | + """ |
| 599 | + def __init__(self, **kwargs): |
| 600 | + self._dict = kwargs |
| 601 | + self.all = SpinesProxy(self._dict) |
| 602 | + |
| 603 | + @classmethod |
| 604 | + def from_dict(cls, d): |
| 605 | + return cls(**d) |
| 606 | + |
| 607 | + def __getstate__(self): |
| 608 | + return self._dict |
| 609 | + |
| 610 | + def __setstate__(self, state): |
| 611 | + self.__init__(**state) |
| 612 | + |
| 613 | + def __getattr__(self, name): |
| 614 | + try: |
| 615 | + return self._dict[name] |
| 616 | + except KeyError: |
| 617 | + raise ValueError( |
| 618 | + f"'Spines' object does not contain a '{name}' spine") |
| 619 | + |
| 620 | + def __getitem__(self, key): |
| 621 | + if isinstance(key, list): |
| 622 | + unknown_keys = [k for k in key if k not in self._dict] |
| 623 | + if unknown_keys: |
| 624 | + raise KeyError(', '.join(unknown_keys)) |
| 625 | + return SpinesProxy({k: v for k, v in self._dict.items() |
| 626 | + if k in key}) |
| 627 | + if isinstance(key, tuple): |
| 628 | + raise ValueError('Multiple spines must be passed as a single list') |
| 629 | + if isinstance(key, slice): |
| 630 | + if key.start is None and key.stop is None and key.step is None: |
| 631 | + return SpinesProxy(self._dict) |
| 632 | + else: |
| 633 | + raise ValueError( |
| 634 | + 'Spines does not support slicing except for the fully ' |
| 635 | + 'open slice [:] to access all spines.') |
| 636 | + return self._dict[key] |
| 637 | + |
| 638 | + def __setitem__(self, key, value): |
| 639 | + # TODO: Do we want to deprecate adding spines? |
| 640 | + self._dict[key] = value |
| 641 | + |
| 642 | + def __delitem__(self, key): |
| 643 | + # TODO: Do we want to deprecate deleting spines? |
| 644 | + del self._dict[key] |
| 645 | + |
| 646 | + def __iter__(self): |
| 647 | + return iter(self._dict) |
| 648 | + |
| 649 | + def __len__(self): |
| 650 | + return len(self._dict) |
0 commit comments