phasorpy 0.6__cp311-cp311-win_amd64.whl → 0.7__cp311-cp311-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phasorpy/__init__.py +1 -1
- phasorpy/_phasorpy.cp311-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +281 -9
- phasorpy/_utils.py +101 -28
- phasorpy/cli.py +19 -1
- phasorpy/cluster.py +10 -16
- phasorpy/color.py +11 -7
- phasorpy/{components.py → component.py} +255 -32
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +117 -7
- phasorpy/experimental.py +8 -10
- phasorpy/io/__init__.py +1 -0
- phasorpy/io/_flimlabs.py +20 -10
- phasorpy/io/_leica.py +3 -1
- phasorpy/io/_ometiff.py +2 -3
- phasorpy/io/_other.py +115 -7
- phasorpy/io/_simfcs.py +41 -16
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +71 -1947
- phasorpy/plot/_functions.py +8 -2
- phasorpy/plot/_lifetime_plots.py +33 -23
- phasorpy/plot/_phasorplot.py +547 -159
- phasorpy/plot/_phasorplot_fret.py +11 -9
- phasorpy/utils.py +21 -10
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/METADATA +2 -2
- phasorpy-0.7.dist-info/RECORD +35 -0
- phasorpy-0.6.dist-info/RECORD +0 -34
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/WHEEL +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/top_level.txt +0 -0
phasorpy/plot/_phasorplot.py
CHANGED
@@ -10,7 +10,7 @@ from collections.abc import Sequence
|
|
10
10
|
from typing import TYPE_CHECKING
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
|
-
from .._typing import Any, ArrayLike, NDArray, IO
|
13
|
+
from .._typing import Any, ArrayLike, Literal, NDArray, IO
|
14
14
|
|
15
15
|
from matplotlib.axes import Axes
|
16
16
|
from matplotlib.figure import Figure
|
@@ -18,8 +18,16 @@ if TYPE_CHECKING:
|
|
18
18
|
import numpy
|
19
19
|
from matplotlib import pyplot
|
20
20
|
from matplotlib.font_manager import FontProperties
|
21
|
+
from matplotlib.legend import Legend
|
21
22
|
from matplotlib.lines import Line2D
|
22
|
-
from matplotlib.patches import
|
23
|
+
from matplotlib.patches import (
|
24
|
+
Arc,
|
25
|
+
Circle,
|
26
|
+
Ellipse,
|
27
|
+
FancyArrowPatch,
|
28
|
+
Polygon,
|
29
|
+
Rectangle,
|
30
|
+
)
|
23
31
|
from matplotlib.path import Path
|
24
32
|
from matplotlib.patheffects import AbstractPathEffect
|
25
33
|
|
@@ -32,19 +40,20 @@ from .._utils import (
|
|
32
40
|
sort_coordinates,
|
33
41
|
update_kwargs,
|
34
42
|
)
|
35
|
-
from ..
|
43
|
+
from ..lifetime import (
|
36
44
|
phasor_from_lifetime,
|
37
45
|
phasor_semicircle,
|
38
46
|
phasor_to_apparent_lifetime,
|
39
|
-
phasor_transform,
|
40
47
|
)
|
48
|
+
from ..phasor import phasor_to_polar, phasor_transform
|
41
49
|
|
42
50
|
GRID_COLOR = '0.5'
|
43
51
|
GRID_LINESTYLE = ':'
|
44
52
|
GRID_LINESTYLE_MAJOR = '-'
|
45
|
-
|
46
|
-
|
53
|
+
GRID_LINEWIDTH = 1.0
|
54
|
+
GRID_LINEWIDTH_MINOR = 0.6
|
47
55
|
GRID_FILL = False
|
56
|
+
GRID_ZORDER = 2
|
48
57
|
|
49
58
|
|
50
59
|
class PhasorPlot:
|
@@ -57,13 +66,18 @@ class PhasorPlot:
|
|
57
66
|
allquadrants : bool, optional
|
58
67
|
Show all quadrants of phasor space.
|
59
68
|
By default, only the first quadrant with universal semicircle is shown.
|
60
|
-
ax : matplotlib
|
69
|
+
ax : matplotlib.axes.Axes, optional
|
61
70
|
Matplotlib axes used for plotting.
|
62
71
|
By default, a new subplot axes is created.
|
63
72
|
frequency : float, optional
|
64
73
|
Laser pulse or modulation frequency in MHz.
|
65
|
-
|
66
|
-
|
74
|
+
pad : float, optional
|
75
|
+
Padding around the plot. The default is 0.05.
|
76
|
+
grid : dict or bool, optional
|
77
|
+
Display universal semicircle (default) or polar grid (allquadrants).
|
78
|
+
If False, no grid is displayed.
|
79
|
+
If a dictionary, it is passed to :py:meth:`PhasorPlot.polar_grid`
|
80
|
+
or :py:meth:`PhasorPlot.semicircle`.
|
67
81
|
**kwargs
|
68
82
|
Additional properties to set on `ax`.
|
69
83
|
|
@@ -77,14 +91,17 @@ class PhasorPlot:
|
|
77
91
|
_ax: Axes
|
78
92
|
"""Matplotlib axes."""
|
79
93
|
|
80
|
-
_limits: tuple[tuple[float, float], tuple[float, float]]
|
81
|
-
"""Axes limits (xmin, xmax), (ymin, ymax)."""
|
82
|
-
|
83
94
|
_full: bool
|
84
95
|
"""Show all quadrants of phasor space."""
|
85
96
|
|
86
|
-
|
87
|
-
"""
|
97
|
+
_labels: bool
|
98
|
+
"""Plot has labels attached."""
|
99
|
+
|
100
|
+
_semicircle_ticks: CircleTicks | None
|
101
|
+
"""Last CircleTicks instance created for semicircle."""
|
102
|
+
|
103
|
+
_unitcircle_ticks: CircleTicks | None
|
104
|
+
"""Last CircleTicks instance created for unit circle."""
|
88
105
|
|
89
106
|
_frequency: float
|
90
107
|
"""Laser pulse or modulation frequency in MHz."""
|
@@ -96,7 +113,8 @@ class PhasorPlot:
|
|
96
113
|
ax: Axes | None = None,
|
97
114
|
*,
|
98
115
|
frequency: float | None = None,
|
99
|
-
grid: bool =
|
116
|
+
grid: dict[str, Any] | bool | None = None,
|
117
|
+
pad: float | None = None,
|
100
118
|
**kwargs: Any,
|
101
119
|
) -> None:
|
102
120
|
# initialize empty phasor plot
|
@@ -104,24 +122,38 @@ class PhasorPlot:
|
|
104
122
|
self._ax.format_coord = ( # type: ignore[method-assign]
|
105
123
|
self._on_format_coord
|
106
124
|
)
|
125
|
+
self._labels = False
|
126
|
+
|
127
|
+
if grid is None:
|
128
|
+
grid_kwargs = {}
|
129
|
+
grid = True
|
130
|
+
if isinstance(grid, dict):
|
131
|
+
grid_kwargs = grid
|
132
|
+
grid = True
|
133
|
+
else:
|
134
|
+
grid_kwargs = {}
|
135
|
+
grid = bool(grid)
|
107
136
|
|
108
137
|
self._semicircle_ticks = None
|
138
|
+
self._unitcircle_ticks = None
|
109
139
|
|
110
140
|
self._full = bool(allquadrants)
|
111
141
|
if self._full:
|
112
|
-
|
113
|
-
|
142
|
+
pad = 0.1 if pad is None else float(abs(pad))
|
143
|
+
xlim = (-1.0 - pad, 1.0 + pad)
|
144
|
+
ylim = (-1.0 - pad, 1.0 + pad)
|
114
145
|
xticks: tuple[float, ...] = (-1.0, -0.5, 0.0, 0.5, 1.0)
|
115
146
|
yticks: tuple[float, ...] = (-1.0, -0.5, 0.0, 0.5, 1.0)
|
116
147
|
if grid:
|
117
|
-
self.polar_grid()
|
148
|
+
self.polar_grid(**grid_kwargs)
|
118
149
|
else:
|
119
|
-
|
120
|
-
|
150
|
+
pad = 0.05 if pad is None else float(abs(pad))
|
151
|
+
xlim = (-pad, 1.0 + pad)
|
152
|
+
ylim = (-pad, 0.65 + pad)
|
121
153
|
xticks = (0.0, 0.2, 0.4, 0.6, 0.8, 1.0)
|
122
154
|
yticks = (0.0, 0.2, 0.4, 0.6)
|
123
155
|
if grid:
|
124
|
-
self.semicircle(frequency=frequency)
|
156
|
+
self.semicircle(frequency=frequency, **grid_kwargs)
|
125
157
|
|
126
158
|
title = 'Phasor plot'
|
127
159
|
if frequency is not None:
|
@@ -135,14 +167,21 @@ class PhasorPlot:
|
|
135
167
|
title=title,
|
136
168
|
xlabel='G, real',
|
137
169
|
ylabel='S, imag',
|
138
|
-
aspect='equal',
|
139
170
|
xlim=xlim,
|
140
171
|
ylim=ylim,
|
141
172
|
xticks=xticks,
|
142
173
|
yticks=yticks,
|
174
|
+
aspect='equal',
|
143
175
|
)
|
144
|
-
|
176
|
+
for key in ('xlim', 'ylim', 'xticks', 'yticks', 'title'):
|
177
|
+
if kwargs[key] is None:
|
178
|
+
del kwargs[key]
|
145
179
|
self._ax.set(**kwargs)
|
180
|
+
# set axis limits after ticks
|
181
|
+
if 'xlim' in kwargs:
|
182
|
+
self._ax.set_xlim(kwargs['xlim'])
|
183
|
+
if 'ylim' in kwargs:
|
184
|
+
self._ax.set_ylim(kwargs['ylim'])
|
146
185
|
|
147
186
|
@property
|
148
187
|
def ax(self) -> Axes:
|
@@ -169,9 +208,22 @@ class PhasorPlot:
|
|
169
208
|
|
170
209
|
def show(self) -> None:
|
171
210
|
"""Display all open figures. Call :py:func:`matplotlib.pyplot.show`."""
|
211
|
+
if self._labels:
|
212
|
+
self._ax.legend()
|
172
213
|
# self.fig.show()
|
173
214
|
pyplot.show()
|
174
215
|
|
216
|
+
def legend(self, **kwargs: Any) -> Legend:
|
217
|
+
"""Add legend to plot.
|
218
|
+
|
219
|
+
Parameters
|
220
|
+
----------
|
221
|
+
**kwargs
|
222
|
+
Keyword arguments passed to :py:func:`matplotlib.axes.Axes.legend`.
|
223
|
+
|
224
|
+
"""
|
225
|
+
return self._ax.legend(**kwargs)
|
226
|
+
|
175
227
|
def save(
|
176
228
|
self,
|
177
229
|
file: str | os.PathLike[Any] | IO[bytes] | None,
|
@@ -251,12 +303,11 @@ class PhasorPlot:
|
|
251
303
|
if label is not None:
|
252
304
|
try:
|
253
305
|
lbl = label[i]
|
306
|
+
if lbl is not None:
|
307
|
+
self._labels = True
|
254
308
|
except IndexError:
|
255
309
|
pass
|
256
310
|
lines = ax.plot(re, im, *args, label=lbl, **kwargs)
|
257
|
-
if label is not None:
|
258
|
-
ax.legend()
|
259
|
-
self._reset_limits()
|
260
311
|
return lines
|
261
312
|
|
262
313
|
def _histogram2d(
|
@@ -267,7 +318,7 @@ class PhasorPlot:
|
|
267
318
|
**kwargs: Any,
|
268
319
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
269
320
|
"""Return two-dimensional histogram of imag versus real coordinates."""
|
270
|
-
update_kwargs(kwargs, range=self.
|
321
|
+
update_kwargs(kwargs, range=(self._ax.get_xlim(), self._ax.get_ylim()))
|
271
322
|
(xmin, xmax), (ymin, ymax) = kwargs['range']
|
272
323
|
assert xmax > xmin and ymax > ymin
|
273
324
|
bins = kwargs.get('bins', 128)
|
@@ -285,13 +336,6 @@ class PhasorPlot:
|
|
285
336
|
**kwargs,
|
286
337
|
)
|
287
338
|
|
288
|
-
def _reset_limits(self) -> None:
|
289
|
-
"""Reset axes limits."""
|
290
|
-
try:
|
291
|
-
self._ax.set(xlim=self._limits[0], ylim=self._limits[1])
|
292
|
-
except AttributeError:
|
293
|
-
pass
|
294
|
-
|
295
339
|
def hist2d(
|
296
340
|
self,
|
297
341
|
real: ArrayLike,
|
@@ -326,7 +370,10 @@ class PhasorPlot:
|
|
326
370
|
if cmax is not None:
|
327
371
|
h[h > cmax] = None
|
328
372
|
self._ax.pcolormesh(xedges, yedges, h.T, **kwargs)
|
329
|
-
|
373
|
+
|
374
|
+
# TODO: create custom labels for pcolormesh?
|
375
|
+
# if 'label' in kwargs:
|
376
|
+
# self._labels = True
|
330
377
|
|
331
378
|
def contour(
|
332
379
|
self,
|
@@ -359,7 +406,10 @@ class PhasorPlot:
|
|
359
406
|
xedges = xedges[:-1] + ((xedges[1] - xedges[0]) / 2.0)
|
360
407
|
yedges = yedges[:-1] + ((yedges[1] - yedges[0]) / 2.0)
|
361
408
|
self._ax.contour(xedges, yedges, h.T, **kwargs)
|
362
|
-
|
409
|
+
|
410
|
+
# TODO: create custom labels for contour?
|
411
|
+
# if 'label' in kwargs:
|
412
|
+
# self._labels = True
|
363
413
|
|
364
414
|
def imshow(
|
365
415
|
self,
|
@@ -369,6 +419,8 @@ class PhasorPlot:
|
|
369
419
|
) -> None:
|
370
420
|
"""Plot an image, for example, a 2D histogram (not implemented).
|
371
421
|
|
422
|
+
This method is not yet implemented and raises NotImplementedError.
|
423
|
+
|
372
424
|
Parameters
|
373
425
|
----------
|
374
426
|
image : array_like
|
@@ -452,14 +504,16 @@ class PhasorPlot:
|
|
452
504
|
)
|
453
505
|
|
454
506
|
if fraction is None:
|
507
|
+
linestyle = kwargs.pop('ls', GRID_LINESTYLE)
|
508
|
+
linewidth = kwargs.pop('lw', GRID_LINEWIDTH)
|
455
509
|
update_kwargs(
|
456
510
|
kwargs,
|
457
511
|
edgecolor=GRID_COLOR if color is None else color,
|
458
|
-
linestyle=
|
459
|
-
linewidth=
|
512
|
+
linestyle=linestyle,
|
513
|
+
linewidth=linewidth,
|
460
514
|
fill=GRID_FILL,
|
461
515
|
)
|
462
|
-
self._ax.add_patch(Polygon(numpy.vstack(
|
516
|
+
self._ax.add_patch(Polygon(numpy.vstack([real, imag]).T, **kwargs))
|
463
517
|
if marker is not None:
|
464
518
|
self._ax.plot(
|
465
519
|
real,
|
@@ -470,18 +524,20 @@ class PhasorPlot:
|
|
470
524
|
label=label_,
|
471
525
|
)
|
472
526
|
if label_ is not None:
|
473
|
-
self.
|
527
|
+
self._labels = True
|
474
528
|
return
|
475
529
|
|
476
530
|
fraction = numpy.asarray(fraction)[indices]
|
531
|
+
linestyle = kwargs.pop('ls', GRID_LINESTYLE)
|
532
|
+
linewidth = kwargs.pop('lw', GRID_LINEWIDTH)
|
477
533
|
update_kwargs(
|
478
534
|
kwargs,
|
479
535
|
color=GRID_COLOR if color is None else color,
|
480
|
-
linestyle=
|
481
|
-
linewidth=
|
536
|
+
linestyle=linestyle,
|
537
|
+
linewidth=linewidth,
|
482
538
|
)
|
483
539
|
center_re, center_im = numpy.average(
|
484
|
-
numpy.vstack(
|
540
|
+
numpy.vstack([real, imag]), axis=-1, weights=fraction
|
485
541
|
)
|
486
542
|
for re, im in zip(real, imag):
|
487
543
|
self._ax.add_line(
|
@@ -498,7 +554,7 @@ class PhasorPlot:
|
|
498
554
|
label=label_,
|
499
555
|
)
|
500
556
|
if label_ is not None:
|
501
|
-
self.
|
557
|
+
self._labels = True
|
502
558
|
|
503
559
|
def line(
|
504
560
|
self,
|
@@ -525,11 +581,10 @@ class PhasorPlot:
|
|
525
581
|
List containing plotted line.
|
526
582
|
|
527
583
|
"""
|
584
|
+
linestyle = kwargs.pop('ls', GRID_LINESTYLE)
|
585
|
+
linewidth = kwargs.pop('lw', GRID_LINEWIDTH)
|
528
586
|
update_kwargs(
|
529
|
-
kwargs,
|
530
|
-
color=GRID_COLOR,
|
531
|
-
linestyle=GRID_LINESTYLE,
|
532
|
-
linewidth=GRID_LINEWIDH,
|
587
|
+
kwargs, color=GRID_COLOR, linestyle=linestyle, linewidth=linewidth
|
533
588
|
)
|
534
589
|
return [self._ax.add_line(Line2D(real, imag, **kwargs))]
|
535
590
|
|
@@ -556,11 +611,13 @@ class PhasorPlot:
|
|
556
611
|
:py:class:`matplotlib.patches.Circle`.
|
557
612
|
|
558
613
|
"""
|
614
|
+
linestyle = kwargs.pop('ls', GRID_LINESTYLE)
|
615
|
+
linewidth = kwargs.pop('lw', GRID_LINEWIDTH)
|
559
616
|
update_kwargs(
|
560
617
|
kwargs,
|
561
618
|
color=GRID_COLOR,
|
562
|
-
linestyle=
|
563
|
-
linewidth=
|
619
|
+
linestyle=linestyle,
|
620
|
+
linewidth=linewidth,
|
564
621
|
fill=GRID_FILL,
|
565
622
|
)
|
566
623
|
self._ax.add_patch(Circle((real, imag), radius, **kwargs))
|
@@ -576,7 +633,7 @@ class PhasorPlot:
|
|
576
633
|
) -> None:
|
577
634
|
"""Draw arrow between points.
|
578
635
|
|
579
|
-
By default, draw a straight arrow with a
|
636
|
+
By default, draw a straight arrow with a ``'-|>'`` style, a mutation
|
580
637
|
scale of 20, and a miter join style.
|
581
638
|
|
582
639
|
Parameters
|
@@ -612,43 +669,55 @@ class PhasorPlot:
|
|
612
669
|
|
613
670
|
def cursor(
|
614
671
|
self,
|
615
|
-
real:
|
616
|
-
imag:
|
672
|
+
real: ArrayLike,
|
673
|
+
imag: ArrayLike,
|
674
|
+
real_limit: ArrayLike | None = None,
|
675
|
+
imag_limit: ArrayLike | None = None,
|
617
676
|
/,
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
677
|
+
*,
|
678
|
+
radius: ArrayLike | None = None,
|
679
|
+
radius_minor: ArrayLike | None = None,
|
680
|
+
angle: ArrayLike | Literal['phase', 'semicircle'] | str | None = None,
|
681
|
+
color: ArrayLike | None = None,
|
682
|
+
label: ArrayLike | None = None,
|
683
|
+
crosshair: bool = False,
|
684
|
+
polar: bool = False,
|
624
685
|
**kwargs: Any,
|
625
686
|
) -> None:
|
626
|
-
"""
|
687
|
+
"""Draw cursor(s) at phasor coordinates.
|
627
688
|
|
628
689
|
Parameters
|
629
690
|
----------
|
630
|
-
real :
|
691
|
+
real : array_like
|
631
692
|
Real component of phasor coordinate.
|
632
|
-
imag :
|
693
|
+
imag : array_like
|
633
694
|
Imaginary component of phasor coordinate.
|
634
|
-
real_limit :
|
695
|
+
real_limit : array_like, optional
|
635
696
|
Real component of limiting phasor coordinate.
|
636
|
-
imag_limit :
|
697
|
+
imag_limit : array_like, optional
|
637
698
|
Imaginary component of limiting phasor coordinate.
|
638
|
-
radius :
|
639
|
-
Radius of
|
640
|
-
radius_minor :
|
699
|
+
radius : array_like, optional
|
700
|
+
Radius of circular cursor.
|
701
|
+
radius_minor : array_like, optional
|
641
702
|
Radius of elliptic cursor along semi-minor axis.
|
642
703
|
By default, `radius_minor` is equal to `radius`, that is,
|
643
704
|
the ellipse is circular.
|
644
|
-
angle :
|
705
|
+
angle : array_like or {'phase', 'semicircle'}, optional
|
645
706
|
Rotation angle of semi-major axis of elliptic cursor in radians.
|
646
|
-
If None
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
707
|
+
If None or 'phase', align the minor axis of the ellipse with
|
708
|
+
the closest tangent on the unit circle.
|
709
|
+
If 'semicircle', align the ellipse with the universal semicircle.
|
710
|
+
color : array_like, optional
|
711
|
+
Color of cursor.
|
712
|
+
label : array_like, optional
|
713
|
+
String label for cursor.
|
714
|
+
crosshair : bool, optional
|
715
|
+
If true, draw polar or Cartesian lines or arcs limited by radius.
|
716
|
+
Else, draw circle or ellipse (default).
|
717
|
+
Only applies if `radius` is provided.
|
718
|
+
polar : bool, optional
|
719
|
+
If true, draw phase line and modulation arc.
|
720
|
+
Else, draw Cartesian lines.
|
652
721
|
**kwargs
|
653
722
|
Additional parameters passed to
|
654
723
|
:py:class:`matplotlib.lines.Line2D`,
|
@@ -663,64 +732,83 @@ class PhasorPlot:
|
|
663
732
|
"""
|
664
733
|
if real_limit is not None and imag_limit is not None:
|
665
734
|
return self.polar_cursor(
|
666
|
-
*
|
667
|
-
*
|
735
|
+
*phasor_to_polar(real, imag),
|
736
|
+
*phasor_to_polar(real_limit, imag_limit),
|
668
737
|
radius=radius,
|
669
738
|
radius_minor=radius_minor,
|
670
739
|
angle=angle,
|
671
|
-
|
740
|
+
color=color,
|
741
|
+
label=label,
|
742
|
+
crosshair=crosshair,
|
743
|
+
polar=polar,
|
672
744
|
**kwargs,
|
673
745
|
)
|
674
746
|
return self.polar_cursor(
|
675
|
-
*
|
747
|
+
*phasor_to_polar(real, imag),
|
676
748
|
radius=radius,
|
677
749
|
radius_minor=radius_minor,
|
678
750
|
angle=angle,
|
679
|
-
|
680
|
-
|
751
|
+
color=color,
|
752
|
+
label=label,
|
753
|
+
crosshair=crosshair,
|
754
|
+
polar=polar,
|
681
755
|
**kwargs,
|
682
756
|
)
|
683
757
|
|
684
758
|
def polar_cursor(
|
685
759
|
self,
|
686
|
-
phase:
|
687
|
-
modulation:
|
688
|
-
phase_limit:
|
689
|
-
modulation_limit:
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
760
|
+
phase: ArrayLike | None = None,
|
761
|
+
modulation: ArrayLike | None = None,
|
762
|
+
phase_limit: ArrayLike | None = None,
|
763
|
+
modulation_limit: ArrayLike | None = None,
|
764
|
+
*,
|
765
|
+
radius: ArrayLike | None = None,
|
766
|
+
radius_minor: ArrayLike | None = None,
|
767
|
+
angle: ArrayLike | Literal['phase', 'semicircle'] | str | None = None,
|
768
|
+
color: ArrayLike | None = None,
|
769
|
+
label: ArrayLike | None = None,
|
770
|
+
crosshair: bool = False,
|
771
|
+
polar: bool = True,
|
694
772
|
**kwargs: Any,
|
695
773
|
) -> None:
|
696
|
-
"""
|
774
|
+
"""Draw cursor(s) at polar coordinates.
|
697
775
|
|
698
776
|
Parameters
|
699
777
|
----------
|
700
|
-
phase :
|
778
|
+
phase : array_like, optional
|
701
779
|
Angular component of polar coordinate in radians.
|
702
|
-
modulation :
|
780
|
+
modulation : array_like, optional
|
703
781
|
Radial component of polar coordinate.
|
704
|
-
phase_limit :
|
782
|
+
phase_limit : array_like, optional
|
705
783
|
Angular component of limiting polar coordinate (in radians).
|
706
|
-
Modulation
|
707
|
-
|
784
|
+
Modulation arcs are drawn between `phase` and `phase_limit`
|
785
|
+
if `polar` is true.
|
786
|
+
modulation_limit : array_like, optional
|
708
787
|
Radial component of limiting polar coordinate.
|
709
|
-
Phase
|
710
|
-
|
711
|
-
|
712
|
-
|
788
|
+
Phase lines are drawn from `modulation` to `modulation_limit`
|
789
|
+
if `polar` is true.
|
790
|
+
radius : array_like, optional
|
791
|
+
Radius of circular cursor.
|
792
|
+
radius_minor : array_like, optional
|
713
793
|
Radius of elliptic cursor along semi-minor axis.
|
714
794
|
By default, `radius_minor` is equal to `radius`, that is,
|
715
795
|
the ellipse is circular.
|
716
|
-
angle :
|
796
|
+
angle : array_like or {'phase', 'semicircle'}, optional
|
717
797
|
Rotation angle of semi-major axis of elliptic cursor in radians.
|
718
|
-
If None
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
798
|
+
If None or 'phase', align the minor axis of the ellipse with
|
799
|
+
the closest tangent on the unit circle.
|
800
|
+
If 'semicircle', align the ellipse with the universal semicircle.
|
801
|
+
color : array_like, optional
|
802
|
+
Color of cursor.
|
803
|
+
label : array_like, optional
|
804
|
+
String label for cursor.
|
805
|
+
crosshair : bool, optional
|
806
|
+
If true, draw polar or Cartesian lines or arcs limited by radius.
|
807
|
+
Else, draw circle or ellipse (default).
|
808
|
+
Only applies if `radius` is provided.
|
809
|
+
polar : bool, optional
|
810
|
+
If true, draw phase line and modulation arc.
|
811
|
+
Else, draw Cartesian lines.
|
724
812
|
**kwargs
|
725
813
|
Additional parameters passed to
|
726
814
|
:py:class:`matplotlib.lines.Line2D`,
|
@@ -733,42 +821,169 @@ class PhasorPlot:
|
|
733
821
|
phasorpy.plot.PhasorPlot.cursor
|
734
822
|
|
735
823
|
"""
|
824
|
+
shape = None
|
825
|
+
if phase is not None:
|
826
|
+
phase = numpy.atleast_1d(phase)
|
827
|
+
if phase.ndim != 1:
|
828
|
+
raise ValueError(f'invalid {phase.ndim=} != 1')
|
829
|
+
shape = phase.shape
|
830
|
+
if modulation is not None:
|
831
|
+
if shape is not None:
|
832
|
+
modulation = numpy.broadcast_to(modulation, shape)
|
833
|
+
else:
|
834
|
+
modulation = numpy.atleast_1d(modulation)
|
835
|
+
if modulation.ndim != 1:
|
836
|
+
raise ValueError(f'invalid {modulation.ndim=} != 1')
|
837
|
+
shape = modulation.shape
|
838
|
+
if shape is None:
|
839
|
+
return
|
840
|
+
|
841
|
+
if phase_limit is not None:
|
842
|
+
phase_limit = numpy.broadcast_to(phase_limit, shape)
|
843
|
+
if modulation_limit is not None:
|
844
|
+
modulation_limit = numpy.broadcast_to(modulation_limit, shape)
|
845
|
+
if radius is not None:
|
846
|
+
radius = numpy.broadcast_to(radius, shape)
|
847
|
+
if radius_minor is not None:
|
848
|
+
radius_minor = numpy.broadcast_to(radius_minor, shape)
|
849
|
+
if angle is not None and not isinstance(angle, str):
|
850
|
+
angle = numpy.broadcast_to(angle, shape)
|
851
|
+
if label is not None:
|
852
|
+
label = numpy.broadcast_to(label, shape)
|
853
|
+
label = [str(c) for c in label]
|
854
|
+
if color is not None:
|
855
|
+
color = numpy.atleast_1d(color)
|
856
|
+
if color.dtype.kind == 'U':
|
857
|
+
color = numpy.broadcast_to(color, shape)
|
858
|
+
color = [str(c) for c in color]
|
859
|
+
else:
|
860
|
+
color = numpy.broadcast_to(color, (shape[0], color.shape[-1]))
|
861
|
+
|
862
|
+
for i in range(shape[0]):
|
863
|
+
|
864
|
+
if color is not None:
|
865
|
+
kwargs['color'] = color[i]
|
866
|
+
if label is not None:
|
867
|
+
kwargs['label'] = label[i]
|
868
|
+
|
869
|
+
self._cursor(
|
870
|
+
phase if phase is None else float(phase[i]),
|
871
|
+
modulation if modulation is None else float(modulation[i]),
|
872
|
+
phase_limit if phase_limit is None else float(phase_limit[i]),
|
873
|
+
(
|
874
|
+
modulation_limit
|
875
|
+
if modulation_limit is None
|
876
|
+
else float(modulation_limit[i])
|
877
|
+
),
|
878
|
+
radius=radius if radius is None else float(radius[i]),
|
879
|
+
radius_minor=(
|
880
|
+
radius_minor
|
881
|
+
if radius_minor is None
|
882
|
+
else float(radius_minor[i])
|
883
|
+
),
|
884
|
+
angle=(
|
885
|
+
angle
|
886
|
+
if (angle is None or isinstance(angle, str))
|
887
|
+
else float(angle[i])
|
888
|
+
),
|
889
|
+
crosshair=crosshair,
|
890
|
+
polar=polar,
|
891
|
+
**kwargs,
|
892
|
+
)
|
893
|
+
|
894
|
+
def _cursor(
|
895
|
+
self,
|
896
|
+
phase: float | None = None,
|
897
|
+
modulation: float | None = None,
|
898
|
+
phase_limit: float | None = None,
|
899
|
+
modulation_limit: float | None = None,
|
900
|
+
*,
|
901
|
+
radius: float | None = None,
|
902
|
+
radius_minor: float | None = None,
|
903
|
+
angle: float | Literal['phase', 'semicircle'] | str | None = None,
|
904
|
+
crosshair: bool = False,
|
905
|
+
polar: bool = True,
|
906
|
+
**kwargs: Any,
|
907
|
+
) -> None:
|
908
|
+
"""Draw single cursor at polar coordinate."""
|
909
|
+
linestyle = kwargs.pop('ls', GRID_LINESTYLE_MAJOR)
|
910
|
+
linewidth = kwargs.pop('lw', GRID_LINEWIDTH)
|
736
911
|
update_kwargs(
|
737
912
|
kwargs,
|
738
913
|
color=GRID_COLOR,
|
739
|
-
linestyle=
|
740
|
-
linewidth=
|
914
|
+
linestyle=linestyle,
|
915
|
+
linewidth=linewidth,
|
741
916
|
fill=GRID_FILL,
|
917
|
+
zorder=GRID_ZORDER,
|
742
918
|
)
|
743
|
-
|
919
|
+
|
744
920
|
ax = self._ax
|
745
921
|
if radius is not None and phase is not None and modulation is not None:
|
746
922
|
x = modulation * math.cos(phase)
|
747
923
|
y = modulation * math.sin(phase)
|
748
924
|
if radius_minor is not None and radius_minor != radius:
|
749
925
|
if angle is None:
|
750
|
-
|
926
|
+
angle = phase
|
927
|
+
elif isinstance(angle, str):
|
928
|
+
if angle == 'phase':
|
929
|
+
angle = phase
|
930
|
+
elif angle == 'semicircle':
|
751
931
|
angle = math.atan2(y, x - 0.5)
|
752
932
|
else:
|
753
|
-
angle
|
933
|
+
raise ValueError(f'invalid {angle=}')
|
754
934
|
angle = math.degrees(angle)
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
935
|
+
|
936
|
+
if not crosshair:
|
937
|
+
# draw elliptical cursor
|
938
|
+
ax.add_patch(
|
939
|
+
Ellipse(
|
940
|
+
(x, y),
|
941
|
+
radius * 2,
|
942
|
+
radius_minor * 2,
|
943
|
+
angle=angle,
|
944
|
+
**kwargs,
|
945
|
+
)
|
762
946
|
)
|
947
|
+
if 'label' in kwargs:
|
948
|
+
self._labels = True
|
949
|
+
return None
|
950
|
+
|
951
|
+
# TODO: implement crosshair intersecting with ellipse?
|
952
|
+
raise ValueError('crosshair not implemented with ellipse')
|
953
|
+
|
954
|
+
if not crosshair:
|
955
|
+
# draw circlar cursor
|
956
|
+
ax.add_patch(Circle((x, y), radius, **kwargs))
|
957
|
+
if 'label' in kwargs:
|
958
|
+
self._labels = True
|
959
|
+
return None
|
960
|
+
|
961
|
+
del kwargs['fill']
|
962
|
+
if not polar:
|
963
|
+
# draw Cartesian crosshair lines limited by radius
|
964
|
+
x0, y0, x1, y1 = _intersect_circle_line(
|
965
|
+
x, y, radius, x, y, x + 1, y
|
763
966
|
)
|
764
|
-
|
967
|
+
ax.add_line(Line2D([x0, x1], [y0, y1], **kwargs))
|
968
|
+
if 'label' in kwargs:
|
969
|
+
self._labels = True
|
970
|
+
del kwargs['label']
|
971
|
+
x0, y0, x1, y1 = _intersect_circle_line(
|
972
|
+
x, y, radius, x, y, x, y + 1
|
973
|
+
)
|
974
|
+
ax.add_line(Line2D([x0, x1], [y0, y1], **kwargs))
|
765
975
|
return None
|
766
|
-
|
767
|
-
if
|
976
|
+
|
977
|
+
if abs(x) < 1e-6 and abs(y) < 1e-6:
|
978
|
+
# phase and modulation not defined at origin
|
768
979
|
return None
|
769
|
-
|
980
|
+
|
981
|
+
# draw crosshair phase line and modulation arc limited by circle
|
770
982
|
x0, y0, x1, y1 = _intersect_circle_line(x, y, radius, 0, 0, x, y)
|
771
|
-
ax.add_line(Line2D(
|
983
|
+
ax.add_line(Line2D([x0, x1], [y0, y1], **kwargs))
|
984
|
+
if 'label' in kwargs:
|
985
|
+
self._labels = True
|
986
|
+
del kwargs['label']
|
772
987
|
x0, y0, x1, y1 = _intersect_circle_circle(
|
773
988
|
0, 0, modulation, x, y, radius
|
774
989
|
)
|
@@ -785,6 +1000,30 @@ class PhasorPlot:
|
|
785
1000
|
)
|
786
1001
|
return None
|
787
1002
|
|
1003
|
+
if not polar:
|
1004
|
+
if phase is None or modulation is None:
|
1005
|
+
return None
|
1006
|
+
|
1007
|
+
x0 = modulation * math.cos(phase)
|
1008
|
+
y0 = modulation * math.sin(phase)
|
1009
|
+
if phase_limit is None or modulation_limit is None:
|
1010
|
+
# draw Cartesian crosshair lines
|
1011
|
+
del kwargs['fill']
|
1012
|
+
ax.add_line(Line2D([x0, x0], [-2, 2], **kwargs))
|
1013
|
+
if 'label' in kwargs:
|
1014
|
+
self._labels = True
|
1015
|
+
del kwargs['label']
|
1016
|
+
ax.add_line(Line2D([-2, 2], [y0, y0], **kwargs))
|
1017
|
+
else:
|
1018
|
+
# draw rectangle
|
1019
|
+
x1 = modulation_limit * math.cos(phase_limit)
|
1020
|
+
y1 = modulation_limit * math.sin(phase_limit)
|
1021
|
+
ax.add_patch(Rectangle((x0, y0), x1 - x0, y1 - y0, **kwargs))
|
1022
|
+
if 'label' in kwargs:
|
1023
|
+
self._labels = True
|
1024
|
+
return None
|
1025
|
+
|
1026
|
+
# TODO: implement filled polar region/rectangle
|
788
1027
|
del kwargs['fill']
|
789
1028
|
for phi in (phase, phase_limit):
|
790
1029
|
if phi is not None:
|
@@ -796,9 +1035,12 @@ class PhasorPlot:
|
|
796
1035
|
else:
|
797
1036
|
x0 = 0
|
798
1037
|
y0 = 0
|
799
|
-
x1 = math.cos(phi)
|
800
|
-
y1 = math.sin(phi)
|
801
|
-
ax.add_line(Line2D(
|
1038
|
+
x1 = math.cos(phi) * 2
|
1039
|
+
y1 = math.sin(phi) * 2
|
1040
|
+
ax.add_line(Line2D([x0, x1], [y0, y1], **kwargs))
|
1041
|
+
if 'label' in kwargs:
|
1042
|
+
self._labels = True
|
1043
|
+
del kwargs['label']
|
802
1044
|
for mod in (modulation, modulation_limit):
|
803
1045
|
if mod is not None:
|
804
1046
|
if phase is not None and phase_limit is not None:
|
@@ -806,7 +1048,8 @@ class PhasorPlot:
|
|
806
1048
|
theta2 = math.degrees(max(phase, phase_limit))
|
807
1049
|
else:
|
808
1050
|
theta1 = 0.0
|
809
|
-
theta2 = 360.0 if self._full else 90.0
|
1051
|
+
theta2 = 360.0 # if self._full else 90.0
|
1052
|
+
# TODO: filling arc objects is not supported
|
810
1053
|
ax.add_patch(
|
811
1054
|
Arc(
|
812
1055
|
(0, 0),
|
@@ -814,51 +1057,176 @@ class PhasorPlot:
|
|
814
1057
|
mod * 2,
|
815
1058
|
theta1=theta1,
|
816
1059
|
theta2=theta2,
|
817
|
-
fill=False,
|
1060
|
+
fill=False,
|
818
1061
|
**kwargs,
|
819
1062
|
)
|
820
1063
|
)
|
1064
|
+
if 'label' in kwargs:
|
1065
|
+
self._labels = True
|
1066
|
+
del kwargs['label']
|
821
1067
|
return None
|
822
1068
|
|
823
|
-
def polar_grid(
|
824
|
-
|
1069
|
+
def polar_grid(
|
1070
|
+
self,
|
1071
|
+
radii: int | Sequence[float] | None = None,
|
1072
|
+
angles: int | Sequence[float] | None = None,
|
1073
|
+
samples: int | None = None,
|
1074
|
+
labels: Sequence[str] | None = None,
|
1075
|
+
ticks: ArrayLike | None = None,
|
1076
|
+
tick_space: ArrayLike | None = None,
|
1077
|
+
tick_format: str | None = None,
|
1078
|
+
**kwargs: Any,
|
1079
|
+
) -> None:
|
1080
|
+
r"""Draw polar coordinate system.
|
825
1081
|
|
826
1082
|
Parameters
|
827
1083
|
----------
|
1084
|
+
radii : int or sequence of float, optional
|
1085
|
+
Position of radial gridlines in range (0, 1].
|
1086
|
+
If an integer, the number of equidistant radial gridlines.
|
1087
|
+
By default, three equidistant radial gridlines are drawn.
|
1088
|
+
The unit circle (radius 1), if included, is drawn in major style.
|
1089
|
+
angles : int or sequence of float, optional
|
1090
|
+
Position of angular gridlines in range [0, 2 pi].
|
1091
|
+
If an integer, the number of equidistant angular gridlines.
|
1092
|
+
By default, 12 equidistant angular gridlines are drawn.
|
1093
|
+
samples : int, optional
|
1094
|
+
Number of vertices of polygon inscribed in unit circle.
|
1095
|
+
By default, no inscribed polygon is drawn.
|
1096
|
+
labels : sequence of str, optional
|
1097
|
+
Tick labels on unit circle.
|
1098
|
+
Labels are placed at equidistant angles if `ticks` are not
|
1099
|
+
provided.
|
1100
|
+
ticks : array_like, optional
|
1101
|
+
Values at which to place tick labels on unit circle.
|
1102
|
+
If `labels` are not provided, `ticks` values formatted with
|
1103
|
+
`tick_format` are used as labels.
|
1104
|
+
If `tick_space` is not provided, tick values are angles in radians.
|
1105
|
+
tick_space : array_like, optional
|
1106
|
+
Values used to convert `ticks` to angles.
|
1107
|
+
For example, the wavelengths used to calculate spectral phasors
|
1108
|
+
or the minimum and maximum wavelengths of a sine-cosine filter.
|
1109
|
+
tick_format : str, optional
|
1110
|
+
Format string for tick values if `labels` is None.
|
1111
|
+
By default, the tick format is "{}".
|
828
1112
|
**kwargs
|
829
1113
|
Parameters passed to
|
830
1114
|
:py:class:`matplotlib.patches.Circle` and
|
831
1115
|
:py:class:`matplotlib.lines.Line2D`.
|
832
1116
|
|
1117
|
+
Raises
|
1118
|
+
------
|
1119
|
+
ValueError
|
1120
|
+
If number of ticks doesn't match number of labels.
|
1121
|
+
If `tick_space` has less than two values.
|
1122
|
+
|
1123
|
+
Notes
|
1124
|
+
-----
|
1125
|
+
Use ``radii=1, angles=4`` to draw major gridlines only.
|
1126
|
+
|
1127
|
+
The values of ticks (:math:`v`) are converted to angles
|
1128
|
+
(:math:`\theta`) using `tick_space` (:math:`s`) according to:
|
1129
|
+
|
1130
|
+
.. math::
|
1131
|
+
\theta = \frac{v - s_0}{s_{-1} + s_1 - 2 s_0} \cdot 2 \pi
|
1132
|
+
|
833
1133
|
"""
|
834
1134
|
ax = self._ax
|
835
|
-
|
836
|
-
|
1135
|
+
minor_kwargs = kwargs.copy()
|
1136
|
+
linestyle = minor_kwargs.pop('ls', GRID_LINESTYLE)
|
1137
|
+
linewidth = minor_kwargs.pop('lw', GRID_LINEWIDTH_MINOR)
|
837
1138
|
update_kwargs(
|
838
|
-
|
1139
|
+
minor_kwargs,
|
839
1140
|
color=GRID_COLOR,
|
840
|
-
linestyle=
|
841
|
-
linewidth=
|
842
|
-
|
1141
|
+
linestyle=linestyle,
|
1142
|
+
linewidth=linewidth,
|
1143
|
+
zorder=GRID_ZORDER,
|
843
1144
|
)
|
844
|
-
|
845
|
-
|
846
|
-
ax.add_patch(Circle((0, 0), 1, fill=False, **kwargs))
|
847
|
-
# minor gridlines
|
848
|
-
kwargs = kwargs_copy
|
1145
|
+
linestyle = kwargs.pop('ls', GRID_LINESTYLE_MAJOR)
|
1146
|
+
linewidth = kwargs.pop('lw', GRID_LINEWIDTH)
|
849
1147
|
update_kwargs(
|
850
1148
|
kwargs,
|
851
1149
|
color=GRID_COLOR,
|
852
|
-
linestyle=
|
853
|
-
linewidth=
|
1150
|
+
linestyle=linestyle,
|
1151
|
+
linewidth=linewidth,
|
1152
|
+
zorder=GRID_ZORDER,
|
1153
|
+
# fill=GRID_FILL,
|
854
1154
|
)
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
1155
|
+
|
1156
|
+
if samples is not None and samples > 1:
|
1157
|
+
angle = numpy.linspace(0, 2 * math.pi, samples, endpoint=False)
|
1158
|
+
xy = numpy.vstack([numpy.cos(angle), numpy.sin(angle)]).T
|
1159
|
+
ax.add_patch(Polygon(xy, fill=False, **kwargs))
|
1160
|
+
|
1161
|
+
if radii is None:
|
1162
|
+
radii = [1 / 3, 2 / 3, 1.0]
|
1163
|
+
elif isinstance(radii, int):
|
1164
|
+
radii = numpy.linspace(0, 1, radii + 1, endpoint=True)[1:].tolist()
|
1165
|
+
for r in radii: # type: ignore[union-attr]
|
1166
|
+
if r < 1e-3:
|
1167
|
+
# skip zero radius
|
1168
|
+
continue
|
1169
|
+
if abs(r - 1.0) < 1e-3:
|
1170
|
+
# unit circle
|
1171
|
+
circle = Circle((0, 0), 1, fill=False, **kwargs)
|
1172
|
+
elif r > 1.0:
|
1173
|
+
continue
|
1174
|
+
else:
|
1175
|
+
# minor circle
|
1176
|
+
circle = Circle((0, 0), r, fill=False, **minor_kwargs)
|
1177
|
+
ax.add_patch(circle)
|
1178
|
+
|
1179
|
+
if angles is None:
|
1180
|
+
angles = 12
|
1181
|
+
if isinstance(angles, int):
|
1182
|
+
angles = numpy.linspace(
|
1183
|
+
0, 2 * math.pi, angles, endpoint=False
|
1184
|
+
).tolist()
|
1185
|
+
for a in angles: # type: ignore[union-attr]
|
1186
|
+
if a < 0 or a > 2 * math.pi:
|
1187
|
+
# skip angles out of range
|
1188
|
+
continue
|
1189
|
+
x = math.cos(a)
|
1190
|
+
y = math.sin(a)
|
1191
|
+
ax.add_line(Line2D([0.0, x], [0.0, y], **minor_kwargs))
|
1192
|
+
|
1193
|
+
if labels is None and ticks is None:
|
1194
|
+
# no labels
|
1195
|
+
return
|
1196
|
+
if ticks is None:
|
1197
|
+
# equidistant labels
|
1198
|
+
assert labels is not None
|
1199
|
+
ticks = numpy.linspace(0, 2 * math.pi, len(labels), endpoint=False)
|
1200
|
+
tick_space = None
|
1201
|
+
elif labels is None:
|
1202
|
+
# use tick values as labels
|
1203
|
+
assert ticks is not None
|
1204
|
+
ticks = numpy.array(ticks, copy=True, ndmin=1)
|
1205
|
+
if tick_format is None:
|
1206
|
+
tick_format = '{}'
|
1207
|
+
labels = [tick_format.format(t) for t in ticks]
|
1208
|
+
ticks = ticks.astype(numpy.float64)
|
1209
|
+
else:
|
1210
|
+
# ticks and labels
|
1211
|
+
ticks = numpy.array(ticks, dtype=numpy.float64, copy=True, ndmin=1)
|
1212
|
+
if ticks.size != len(labels):
|
1213
|
+
raise ValueError(f'{ticks.size=} != {len(labels)=}')
|
1214
|
+
|
1215
|
+
if tick_space is not None:
|
1216
|
+
tick_space = numpy.asarray(tick_space, dtype=numpy.float64)
|
1217
|
+
if tick_space.ndim != 1 or tick_space.size < 2:
|
1218
|
+
raise ValueError(
|
1219
|
+
f'invalid {tick_space.ndim=} or {tick_space.size=} < 2'
|
1220
|
+
)
|
1221
|
+
assert isinstance(ticks, numpy.ndarray) # for mypy
|
1222
|
+
ticks -= tick_space[0]
|
1223
|
+
ticks /= tick_space[-1] + tick_space[1] - 2 * tick_space[0]
|
1224
|
+
ticks *= 2 * math.pi
|
1225
|
+
|
1226
|
+
real = numpy.cos(ticks)
|
1227
|
+
imag = numpy.sin(ticks)
|
1228
|
+
self._unitcircle_ticks = CircleTicks(labels=labels)
|
1229
|
+
ax.plot(real, imag, path_effects=[self._unitcircle_ticks], **kwargs)
|
862
1230
|
|
863
1231
|
def semicircle(
|
864
1232
|
self,
|
@@ -901,19 +1269,25 @@ class PhasorPlot:
|
|
901
1269
|
|
902
1270
|
Returns
|
903
1271
|
-------
|
904
|
-
list
|
1272
|
+
list of matplotlib.lines.Line2D
|
905
1273
|
Lines representing plotted semicircle and ticks.
|
906
1274
|
|
907
1275
|
"""
|
908
1276
|
if frequency is not None:
|
909
1277
|
self._frequency = float(frequency)
|
910
1278
|
|
1279
|
+
linestyle = kwargs.pop('ls', GRID_LINESTYLE_MAJOR)
|
1280
|
+
linewidth = kwargs.pop('lw', GRID_LINEWIDTH)
|
911
1281
|
update_kwargs(
|
912
1282
|
kwargs,
|
1283
|
+
linestyle=linestyle,
|
1284
|
+
linewidth=linewidth,
|
913
1285
|
color=GRID_COLOR,
|
914
|
-
|
915
|
-
linewidth=GRID_LINEWIDH,
|
1286
|
+
zorder=GRID_ZORDER,
|
916
1287
|
)
|
1288
|
+
if 'label' in kwargs:
|
1289
|
+
self._labels = True
|
1290
|
+
|
917
1291
|
if phasor_reference is not None:
|
918
1292
|
polar_reference = phasor_to_polar_scalar(*phasor_reference)
|
919
1293
|
if polar_reference is None:
|
@@ -949,10 +1323,13 @@ class PhasorPlot:
|
|
949
1323
|
)
|
950
1324
|
)
|
951
1325
|
|
1326
|
+
kwargs.pop('label', None) # don't pass label to ticks
|
1327
|
+
kwargs.pop('capstyle', None)
|
1328
|
+
|
952
1329
|
if frequency is not None and polar_reference == (0.0, 1.0):
|
953
1330
|
# draw ticks and labels
|
954
1331
|
lifetime, labels = _semicircle_ticks(frequency, lifetime, labels)
|
955
|
-
self._semicircle_ticks =
|
1332
|
+
self._semicircle_ticks = CircleTicks((0.5, 0.0), labels=labels)
|
956
1333
|
lines.extend(
|
957
1334
|
ax.plot(
|
958
1335
|
*phasor_transform(
|
@@ -963,7 +1340,6 @@ class PhasorPlot:
|
|
963
1340
|
**kwargs,
|
964
1341
|
)
|
965
1342
|
)
|
966
|
-
self._reset_limits()
|
967
1343
|
return lines
|
968
1344
|
|
969
1345
|
def _on_format_coord(self, x: float, y: float) -> str:
|
@@ -979,11 +1355,13 @@ class PhasorPlot:
|
|
979
1355
|
return ' '.join(reversed(ret))
|
980
1356
|
|
981
1357
|
|
982
|
-
class
|
983
|
-
"""Draw ticks on universal semicircle.
|
1358
|
+
class CircleTicks(AbstractPathEffect):
|
1359
|
+
"""Draw ticks on unit circle or universal semicircle.
|
984
1360
|
|
985
1361
|
Parameters
|
986
1362
|
----------
|
1363
|
+
origin : (float, float), optional
|
1364
|
+
Origin of circle.
|
987
1365
|
size : float, optional
|
988
1366
|
Length of tick in dots.
|
989
1367
|
The default is ``rcParams['xtick.major.size']``.
|
@@ -995,23 +1373,31 @@ class SemicircleTicks(AbstractPathEffect):
|
|
995
1373
|
|
996
1374
|
"""
|
997
1375
|
|
1376
|
+
_origin: tuple[float, float] # origin of circle
|
998
1377
|
_size: float # tick length
|
999
1378
|
_labels: tuple[str, ...] # tick labels
|
1000
1379
|
_gc: dict[str, Any] # keywords passed to _update_gc
|
1001
1380
|
|
1002
1381
|
def __init__(
|
1003
1382
|
self,
|
1383
|
+
origin: tuple[float, float] | None = None,
|
1384
|
+
/,
|
1004
1385
|
size: float | None = None,
|
1005
1386
|
labels: Sequence[str] | None = None,
|
1006
1387
|
**kwargs: Any,
|
1007
1388
|
) -> None:
|
1008
1389
|
super().__init__((0.0, 0.0))
|
1009
1390
|
|
1391
|
+
if origin is None:
|
1392
|
+
self._origin = 0.0, 0.0
|
1393
|
+
else:
|
1394
|
+
self._origin = float(origin[0]), float(origin[1])
|
1395
|
+
|
1010
1396
|
if size is None:
|
1011
1397
|
self._size = pyplot.rcParams['xtick.major.size']
|
1012
1398
|
else:
|
1013
1399
|
self._size = size
|
1014
|
-
if labels is None or
|
1400
|
+
if labels is None or len(labels) == 0:
|
1015
1401
|
self._labels = ()
|
1016
1402
|
else:
|
1017
1403
|
self._labels = tuple(labels)
|
@@ -1024,7 +1410,7 @@ class SemicircleTicks(AbstractPathEffect):
|
|
1024
1410
|
|
1025
1411
|
@labels.setter
|
1026
1412
|
def labels(self, value: Sequence[str] | None, /) -> None:
|
1027
|
-
if value is None
|
1413
|
+
if value is None:
|
1028
1414
|
self._labels = ()
|
1029
1415
|
else:
|
1030
1416
|
self._labels = tuple(value)
|
@@ -1050,7 +1436,7 @@ class SemicircleTicks(AbstractPathEffect):
|
|
1050
1436
|
# approximate half size of 'x'
|
1051
1437
|
fontsize = renderer.points_to_pixels(font.get_size_in_points()) / 4
|
1052
1438
|
size = renderer.points_to_pixels(self._size)
|
1053
|
-
origin = affine.transform(
|
1439
|
+
origin = affine.transform((self._origin,))
|
1054
1440
|
|
1055
1441
|
transpath = affine.transform_path(tpath)
|
1056
1442
|
polys = transpath.to_polygons(closed_only=False)
|
@@ -1088,6 +1474,8 @@ class SemicircleTicks(AbstractPathEffect):
|
|
1088
1474
|
# TODO: get rendered text size from matplotlib.text.Text?
|
1089
1475
|
# this did not work:
|
1090
1476
|
# Text(d[i,0], h - d[i,1], label, ha='center', va='center')
|
1477
|
+
if not s:
|
1478
|
+
continue
|
1091
1479
|
x = x + fontsize * len(s.split()[0]) * (dx - 1.0)
|
1092
1480
|
y = h - y + fontsize
|
1093
1481
|
renderer.draw_text(gc0, x, y, s, font, 0.0)
|