|
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
|
|
|
|