-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathpolar.py
More file actions
332 lines (304 loc) · 13 KB
/
polar.py
File metadata and controls
332 lines (304 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
#!/usr/bin/env python3
"""
Polar axes using azimuth and radius instead of *x* and *y*.
"""
import inspect
import matplotlib.projections.polar as mpolar
import numpy as np
from .. import constructor
from .. import ticker as pticker
from ..config import rc
from ..internals import ic # noqa: F401
from ..internals import _not_none, _pop_rc, docstring
from . import plot, shared
__all__ = ['PolarAxes']
# Format docstring
_format_docstring = """
r0 : float, default: 0
The radial origin.
theta0 : {'N', 'NW', 'W', 'SW', 'S', 'SE', 'E', 'NE'}, optional
The zero azimuth location.
thetadir : {1, -1, 'anticlockwise', 'counterclockwise', 'clockwise'}, optional
The positive azimuth direction. Clockwise corresponds to
``-1`` and anticlockwise corresponds to ``1``.
thetamin, thetamax : float, optional
The lower and upper azimuthal bounds in degrees. If
``thetamax != thetamin + 360``, this produces a sector plot.
thetalim : 2-tuple of float or None, optional
Specifies `thetamin` and `thetamax` at once.
rmin, rmax : float, optional
The inner and outer radial limits. If ``r0 != rmin``, this
produces an annular plot.
rlim : 2-tuple of float or None, optional
Specifies `rmin` and `rmax` at once.
rborder : bool, optional
Whether to draw the polar axes border. Visibility of the "inner"
radial spine and "start" and "end" azimuthal spines is controlled
automatically by matplotlib.
thetagrid, rgrid, grid : bool, optional
Whether to draw major gridlines for the azimuthal and radial axis.
Use the keyword `grid` to toggle both.
thetagridminor, rgridminor, gridminor : bool, optional
Whether to draw minor gridlines for the azimuthal and radial axis.
Use the keyword `gridminor` to toggle both.
thetagridcolor, rgridcolor, gridcolor : color-spec, optional
Color for the major and minor azimuthal and radial gridlines.
Use the keyword `gridcolor` to set both at once.
thetalocator, rlocator : locator-spec, optional
Used to determine the azimuthal and radial gridline positions.
Passed to the `~proplot.constructor.Locator` constructor. Can be
float, list of float, string, or `matplotlib.ticker.Locator` instance.
thetalines, rlines
Aliases for `thetalocator`, `rlocator`.
thetalocator_kw, rlocator_kw : dict-like, optional
The azimuthal and radial locator settings. Passed to
`~proplot.constructor.Locator`.
thetaminorlocator, rminorlocator : optional
As for `thetalocator`, `rlocator`, but for the minor gridlines.
thetaminorticks, rminorticks : optional
Aliases for `thetaminorlocator`, `rminorlocator`.
thetaminorlocator_kw, rminorlocator_kw
As for `thetalocator_kw`, `rlocator_kw`, but for the minor locator.
rlabelpos : float, optional
The azimuth at which radial coordinates are labeled.
thetaformatter, rformatter : formatter-spec, optional
Used to determine the azimuthal and radial label format.
Passed to the `~proplot.constructor.Formatter` constructor.
Can be string, list of string, or `matplotlib.ticker.Formatter`
instance. Use ``[]``, ``'null'``, or ``'none'`` for no labels.
thetalabels, rlabels : optional
Aliases for `thetaformatter`, `rformatter`.
thetaformatter_kw, rformatter_kw : dict-like, optional
The azimuthal and radial label formatter settings. Passed to
`~proplot.constructor.Formatter`.
color : color-spec, default: :rc:`meta.color`
Color for the axes edge. Propagates to `labelcolor` unless specified
otherwise (similar to `proplot.axes.CartesianAxes.format`).
labelcolor, gridlabelcolor : color-spec, default: `color` or :rc:`grid.labelcolor`
Color for the gridline labels.
labelpad, gridlabelpad : unit-spec, default: :rc:`grid.labelpad`
The padding between the axes edge and the radial and azimuthal labels.
%(units.pt)s
labelsize, gridlabelsize : unit-spec or str, default: :rc:`grid.labelsize`
Font size for the gridline labels.
%(units.pt)s
labelweight, gridlabelweight : str, default: :rc:`grid.labelweight`
Font weight for the gridline labels.
"""
docstring._snippet_manager['polar.format'] = _format_docstring
class PolarAxes(shared._SharedAxes, plot.PlotAxes, mpolar.PolarAxes):
"""
Axes subclass for plotting in polar coordinates. Adds the `~PolarAxes.format`
method and overrides several existing methods.
Important
---------
This axes subclass can be used by passing ``proj='polar'``
to axes-creation commands like `~proplot.figure.Figure.add_axes`,
`~proplot.figure.Figure.add_subplot`, and `~proplot.figure.Figure.subplots`.
"""
_name = 'polar'
@docstring._snippet_manager
def __init__(self, *args, **kwargs):
"""
Parameters
----------
*args
Passed to `matplotlib.axes.Axes`.
%(polar.format)s
Other parameters
----------------
%(axes.format)s
%(rc.init)s
See also
--------
PolarAxes.format
proplot.axes.Axes
proplot.axes.PlotAxes
matplotlib.projections.PolarAxes
proplot.figure.Figure.subplot
proplot.figure.Figure.add_subplot
"""
# Set tick length to zero so azimuthal labels are not too offset
# Change default radial axis formatter but keep default theta one
super().__init__(*args, **kwargs)
self.yaxis.set_major_formatter(pticker.AutoFormatter())
self.yaxis.isDefault_majfmt = True
for axis in (self.xaxis, self.yaxis):
axis.set_tick_params(which='both', size=0)
def _update_formatter(self, x, *, formatter=None, formatter_kw=None):
"""
Update the gridline label formatter.
"""
# Tick formatter and toggling
axis = getattr(self, x + 'axis')
formatter_kw = formatter_kw or {}
if formatter is not None:
formatter = constructor.Formatter(formatter, **formatter_kw) # noqa: E501
axis.set_major_formatter(formatter)
def _update_limits(self, x, *, min_=None, max_=None, lim=None):
"""
Update the limits.
"""
# Try to use public API where possible
r = 'theta' if x == 'x' else 'r'
min_, max_ = self._min_max_lim(r, min_, max_, lim)
if min_ is not None:
getattr(self, f'set_{r}min')(min_)
if max_ is not None:
getattr(self, f'set_{r}max')(max_)
def _update_locators(
self, x, *,
locator=None, locator_kw=None, minorlocator=None, minorlocator_kw=None,
):
"""
Update the gridline locator.
"""
# TODO: Add minor tick 'toggling' as with cartesian axes?
# NOTE: Must convert theta locator input to radians, then back to deg.
r = 'theta' if x == 'x' else 'r'
axis = getattr(self, x + 'axis')
min_ = getattr(self, f'get_{r}min')()
max_ = getattr(self, f'get_{r}max')()
for i, (loc, loc_kw) in enumerate(
zip((locator, minorlocator), (locator_kw, minorlocator_kw))
):
if loc is None:
continue
# Get locator
loc_kw = loc_kw or {}
loc = constructor.Locator(loc, **loc_kw)
# Sanitize values
array = loc.tick_values(min_, max_)
array = array[(array >= min_) & (array <= max_)]
if x == 'x':
array = np.deg2rad(array)
if np.isclose(array[-1], min_ + 2 * np.pi): # exclusive if 360 deg
array = array[:-1]
# Assign fixed location
loc = constructor.Locator(array) # convert to FixedLocator
if i == 0:
axis.set_major_locator(loc)
else:
axis.set_minor_locator(loc)
@docstring._snippet_manager
def format(
self, *, r0=None, theta0=None, thetadir=None,
thetamin=None, thetamax=None, thetalim=None,
rmin=None, rmax=None, rlim=None,
thetagrid=None, rgrid=None,
thetagridminor=None, rgridminor=None,
thetagridcolor=None, rgridcolor=None,
rlabelpos=None, rscale=None, rborder=None,
thetalocator=None, rlocator=None, thetalines=None, rlines=None,
thetalocator_kw=None, rlocator_kw=None,
thetaminorlocator=None, rminorlocator=None, thetaminorlines=None, rminorlines=None, # noqa: E501
thetaminorlocator_kw=None, rminorlocator_kw=None,
thetaformatter=None, rformatter=None, thetalabels=None, rlabels=None,
thetaformatter_kw=None, rformatter_kw=None,
labelpad=None, labelsize=None, labelcolor=None, labelweight=None,
**kwargs
):
"""
Modify axes limits, radial and azimuthal gridlines, and more. Note that
all of the ``theta`` arguments are specified in degrees, not radians.
Parameters
----------
%(polar.format)s
Other parameters
----------------
%(axes.format)s
%(figure.format)s
%(rc.format)s
See also
--------
proplot.axes.Axes.format
proplot.config.Configurator.context
"""
# NOTE: Here we capture 'label.pad' rc argument normally used for
# x and y axis labels as shorthand for 'tick.labelpad'.
rc_kw, rc_mode = _pop_rc(kwargs)
labelcolor = _not_none(labelcolor, kwargs.get('color', None))
with rc.context(rc_kw, mode=rc_mode):
# Not mutable default args
thetalocator_kw = thetalocator_kw or {}
thetaminorlocator_kw = thetaminorlocator_kw or {}
thetaformatter_kw = thetaformatter_kw or {}
rlocator_kw = rlocator_kw or {}
rminorlocator_kw = rminorlocator_kw or {}
rformatter_kw = rformatter_kw or {}
# Flexible input
thetalocator = _not_none(thetalines=thetalines, thetalocator=thetalocator)
thetaformatter = _not_none(thetalabels=thetalabels, thetaformatter=thetaformatter) # noqa: E501
thetaminorlocator = _not_none(thetaminorlines=thetaminorlines, thetaminorlocator=thetaminorlocator) # noqa: E501
rlocator = _not_none(rlines=rlines, rlocator=rlocator)
rformatter = _not_none(rlabels=rlabels, rformatter=rformatter)
rminorlocator = _not_none(rminorlines=rminorlines, rminorlocator=rminorlocator) # noqa: E501
# Special radius settings
if r0 is not None:
self.set_rorigin(r0)
if rlabelpos is not None:
self.set_rlabel_position(rlabelpos)
if rscale is not None:
self.set_rscale(rscale)
if rborder is not None:
self.spines['polar'].set_visible(bool(rborder))
# Special azimuth settings
if theta0 is not None:
self.set_theta_zero_location(theta0)
if thetadir is not None:
self.set_theta_direction(thetadir)
# Loop over axes
for (
x,
min_,
max_,
lim,
grid,
gridminor,
gridcolor,
locator,
locator_kw,
formatter,
formatter_kw,
minorlocator,
minorlocator_kw,
) in zip(
('x', 'y'),
(thetamin, rmin),
(thetamax, rmax),
(thetalim, rlim),
(thetagrid, rgrid),
(thetagridminor, rgridminor),
(thetagridcolor, rgridcolor),
(thetalocator, rlocator),
(thetalocator_kw, rlocator_kw),
(thetaformatter, rformatter),
(thetaformatter_kw, rformatter_kw),
(thetaminorlocator, rminorlocator),
(thetaminorlocator_kw, rminorlocator_kw),
):
# Axis limits
self._update_limits(x, min_=min_, max_=max_, lim=lim)
# Axis tick settings
# NOTE: Here use 'grid.labelpad' instead of 'tick.labelpad'. Default
# offset for grid labels is larger than for tick labels.
self._update_ticks(
x, grid=grid, gridminor=gridminor, gridcolor=gridcolor,
gridpad=True, labelpad=labelpad, labelcolor=labelcolor,
labelsize=labelsize, labelweight=labelweight,
)
# Axis locator
self._update_locators(
x, locator=locator, locator_kw=locator_kw,
minorlocator=minorlocator, minorlocator_kw=minorlocator_kw
)
# Axis formatter
self._update_formatter(
x, formatter=formatter, formatter_kw=formatter_kw
)
# Parent format method
super().format(rc_kw=rc_kw, rc_mode=rc_mode, **kwargs)
# Apply signature obfuscation after storing previous signature
# NOTE: This is needed for __init__
PolarAxes._format_signatures[PolarAxes] = inspect.signature(PolarAxes.format)
PolarAxes.format = docstring._obfuscate_kwargs(PolarAxes.format)