Passed
Push — main ( a79885...50e5ea )
by Douglas
02:25
created

bound_ticks.AxisTicks.__str__()   A

Complexity

Conditions 3

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
import typing
2
from dataclasses import dataclass
3
from typing import Optional
4
5
import numpy as np
6
from matplotlib.axes import Axes
7
8
9
def _oround(x: float, digits: Optional[int] = None) -> float:
10
    return x if digits is None else round(x, digits)
11
12
13
@dataclass(frozen=True, repr=True, order=True)
14
class AxisTicks:
15
    """
16
    Calculates new bounds for an axis based on the ticks.
17
18
    Attributes:
19
         floor: If None, sets the minimum bound based on the data
20
         ceiling: If None, sets the maximum bound based on the data
21
         rounding_digits: Minor argument that rounds the final bounds to some number of digits
22
    """
23
24
    floor: Optional[float] = None
25
    ceiling: Optional[float] = None
26
    rounding_digits: Optional[float] = None
27
28
    def __str__(self) -> str:
29
        return "ticks({}, {})".format(
30
            "min" if self.floor is None else round(self.floor, 5),
31
            "max" if self.ceiling is None else round(self.ceiling, 5),
32
        )
33
34
    def adjusted(self, ticks: np.array, bottom: float, top: float) -> typing.Tuple[float, float]:
35
        if len(ticks) < 2:
36
            return bottom, top
37
        floor = ticks[0] if self.floor is None else self.floor
38
        ceiling = ticks[-1] if self.ceiling is None else self.ceiling
39
        return (
40
            _oround(floor, self.rounding_digits),
41
            _oround(ceiling, self.rounding_digits),
42
        )
43
44
45
@dataclass(frozen=True, repr=True, order=True)
46
class TickBounder:
47
    """
48
    Forces the limits of a Matplotlib Axes to end at major or minor ticks.
49
    Pass AxisTicks as the x_ticks and y_ticks constructor arguments to perform this.
50
51
    This example will bound maximum width and height of the Axes to the smallest tick that fits the data,
52
    and will set the minimum width and height to 0.
53
54
    Example:
55
        For example::
56
57
            ticker = TickBounder(x_ticks = AxisTicks(floor=0), y_ticks = AxisTicks(floor=0))
58
            ticker.adjust(ax)
59
60
        You can see the proposed new bound without changing the Axes using::
61
62
            ticker.adjusted(ax)  # returns a ((x0, x1), (y0, y1)) tuple
63
    """
64
65
    x_ticks: Optional[AxisTicks] = None
66
    y_ticks: Optional[AxisTicks] = None
67
    use_major_ticks: bool = True
68
69
    def adjust(self, ax: Axes) -> Axes:
70
        x_adj, y_adj = self.adjusted(ax)
71
        ax.set_xlim(*x_adj)
72
        ax.set_ylim(*y_adj)
73
        return ax
74
75
    def adjusted(
76
        self, ax: Axes
77
    ) -> typing.Tuple[typing.Tuple[float, float], typing.Tuple[float, float]]:
78
        xmin, xmax, ymin, ymax = (
79
            list(ax.get_xlim())[0],
80
            list(ax.get_xlim())[1],
81
            list(ax.get_ylim())[0],
82
            list(ax.get_ylim())[1],
83
        )
84
        xs = ax.xaxis.get_majorticklocs() if self.use_major_ticks else ax.get_xticks()
85
        ys = ax.yaxis.get_majorticklocs() if self.use_major_ticks else ax.get_yticks()
86
        return (
87
            (xmin, xmax) if self.x_ticks is None else self.x_ticks.adjusted(xs, xmin, xmax),
88
            (ymin, ymax) if self.y_ticks is None else self.y_ticks.adjusted(ys, ymin, ymax),
89
        )
90
91
92
__all__ = ["AxisTicks", "TickBounder"]
93