Skip to content

Commit 8c430e8

Browse files
committed
Add a custom RadialTick for polar plots.
Just like ThetaTick, these allow for text and tick to be correctly perpendicular to the spine.
1 parent 30b4a6c commit 8c430e8

File tree

1 file changed

+101
-41
lines changed

1 file changed

+101
-41
lines changed

lib/matplotlib/projections/polar.py

Lines changed: 101 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,96 @@ def view_limits(self, vmin, vmax):
370370
return mtransforms.nonsingular(min(0, vmin), vmax)
371371

372372

373+
class RadialTick(maxis.YTick):
374+
"""
375+
A radial-axis tick.
376+
377+
This subclass of `YTick` provides radial ticks with some small modification
378+
to their re-positioning such that ticks are rotated based on axes limits.
379+
This results in ticks that are correctly perpendicular to the spine. Labels
380+
are also rotated to be perpendicular to the spine.
381+
"""
382+
def _get_text1(self):
383+
t = maxis.YTick._get_text1(self)
384+
t.set_rotation_mode('anchor')
385+
return t
386+
387+
def _get_text2(self):
388+
t = maxis.YTick._get_text2(self)
389+
t.set_rotation_mode('anchor')
390+
return t
391+
392+
def update_position(self, loc):
393+
maxis.YTick.update_position(self, loc)
394+
axes = self.axes
395+
thetamin = axes.get_thetamin()
396+
thetamax = axes.get_thetamax()
397+
direction = axes.get_theta_direction()
398+
offset_rad = axes.get_theta_offset()
399+
offset = np.rad2deg(offset_rad)
400+
full = _is_full_circle_deg(thetamin, thetamax)
401+
402+
if full:
403+
angle = axes.get_rlabel_position() * direction + offset - 90
404+
tick_angle = np.deg2rad(angle)
405+
else:
406+
angle = thetamin * direction + offset - 90
407+
if direction > 0:
408+
tick_angle = np.deg2rad(angle)
409+
else:
410+
tick_angle = np.deg2rad(angle + 180)
411+
if self.label1On:
412+
self.label1.set_rotation(angle + self._labelrotation)
413+
if self.tick1On:
414+
marker = self.tick1line.get_marker()
415+
if marker == mmarkers.TICKLEFT:
416+
trans = (mtransforms.Affine2D()
417+
.scale(1.0, 1.0)
418+
.rotate(tick_angle))
419+
elif marker == '_':
420+
trans = (mtransforms.Affine2D()
421+
.scale(1.0, 1.0)
422+
.rotate(tick_angle + np.pi / 2))
423+
elif marker == mmarkers.TICKRIGHT:
424+
trans = (mtransforms.Affine2D()
425+
.scale(-1.0, 1.0)
426+
.rotate(tick_angle))
427+
else:
428+
# Don't modify custom tick line markers.
429+
trans = self.tick1line._marker._transform
430+
self.tick1line._marker._transform = trans
431+
432+
if full:
433+
self.label2On = False
434+
self.tick2On = False
435+
else:
436+
angle = thetamax * direction + offset - 90
437+
if direction > 0:
438+
tick_angle = np.deg2rad(angle)
439+
else:
440+
tick_angle = np.deg2rad(angle + 180)
441+
if self.label2On:
442+
self.label2.set_rotation(angle + self._labelrotation)
443+
if self.tick2On:
444+
marker = self.tick2line.get_marker()
445+
if marker == mmarkers.TICKLEFT:
446+
trans = (mtransforms.Affine2D()
447+
.scale(1.0, 1.0)
448+
.rotate(tick_angle))
449+
elif marker == '_':
450+
trans = (mtransforms.Affine2D()
451+
.scale(1.0, 1.0)
452+
.rotate(tick_angle + np.pi / 2))
453+
elif marker == mmarkers.TICKRIGHT:
454+
trans = (mtransforms.Affine2D()
455+
.scale(-1.0, 1.0)
456+
.rotate(tick_angle))
457+
else:
458+
# Don't modify custom tick line markers.
459+
trans = self.tick2line._marker._transform
460+
self.tick2line._marker._transform = trans
461+
462+
373463
class RadialAxis(maxis.YAxis):
374464
"""
375465
A radial Axis.
@@ -385,7 +475,7 @@ def _get_tick(self, major):
385475
tick_kw = self._major_tick_kw
386476
else:
387477
tick_kw = self._minor_tick_kw
388-
return maxis.YTick(self.axes, 0, '', major=major, **tick_kw)
478+
return RadialTick(self.axes, 0, '', major=major, **tick_kw)
389479

390480
def _wrap_locator_formatter(self):
391481
self.set_major_locator(RadialLocator(self.get_major_locator(),
@@ -657,50 +747,16 @@ def get_yaxis_transform(self, which='grid'):
657747
def get_yaxis_text1_transform(self, pad):
658748
thetamin, thetamax = self._realViewLim.intervalx
659749
full = _is_full_circle_rad(thetamin, thetamax)
660-
if full:
661-
angle = self.get_rlabel_position()
750+
if self.get_theta_direction() > 0 or full:
751+
return self._yaxis_text_transform, 'center', 'left'
662752
else:
663-
angle = np.rad2deg(thetamin)
664-
if angle < 0:
665-
angle += 360
666-
angle %= 360
667-
668-
# NOTE: Due to a bug, previous code always used bottom left, contrary
669-
# to its original intentions here.
670-
valign = [['top', 'bottom', 'bottom', 'top'],
671-
# ['bottom', 'bottom', 'top', 'top']]
672-
['bottom', 'bottom', 'bottom', 'bottom']]
673-
halign = [['left', 'left', 'right', 'right'],
674-
# ['left', 'right', 'right', 'left']]
675-
['left', 'left', 'left', 'left']]
676-
677-
ind = np.digitize([angle], np.arange(0, 361, 90))[0] - 1
678-
679-
return self._yaxis_text_transform, valign[full][ind], halign[full][ind]
753+
return self._yaxis_text_transform, 'center', 'right'
680754

681755
def get_yaxis_text2_transform(self, pad):
682-
thetamin, thetamax = self._realViewLim.intervalx
683-
full = _is_full_circle_rad(thetamin, thetamax)
684-
if full:
685-
angle = self.get_rlabel_position()
756+
if self.get_theta_direction() > 0:
757+
return self._yaxis_text_transform, 'center', 'right'
686758
else:
687-
angle = np.rad2deg(thetamax)
688-
if angle < 0:
689-
angle += 360
690-
angle %= 360
691-
692-
# NOTE: Due to a bug, previous code always used top right, contrary to
693-
# its original intentions here.
694-
valign = [['bottom', 'top', 'top', 'bottom'],
695-
# ['top', 'top', 'bottom', 'bottom']]
696-
['top', 'top', 'top', 'top']]
697-
halign = [['right', 'right', 'left', 'left'],
698-
# ['right', 'left', 'left', 'right']]
699-
['right', 'right', 'right', 'right']]
700-
701-
ind = np.digitize([angle], np.arange(0, 361, 90))[0] - 1
702-
703-
return self._yaxis_text_transform, valign[full][ind], halign[full][ind]
759+
return self._yaxis_text_transform, 'center', 'left'
704760

705761
def draw(self, *args, **kwargs):
706762
thetamin, thetamax = self._realViewLim.intervalx
@@ -845,6 +901,9 @@ def set_theta_direction(self, direction):
845901
raise ValueError(
846902
"direction must be 1, -1, clockwise or counterclockwise")
847903
self._direction.invalidate()
904+
# FIXME: Why is this needed? Even though the tick label gets
905+
# re-created, the alignment is not correctly updated without a reset.
906+
self.yaxis.reset_ticks()
848907

849908
def get_theta_direction(self):
850909
"""
@@ -901,6 +960,7 @@ def set_rlabel_position(self, value):
901960
The angular position of the radius labels in degrees.
902961
"""
903962
self._r_label_position.clear().translate(np.deg2rad(value), 0.0)
963+
self.yaxis.reset_ticks()
904964

905965
def set_yscale(self, *args, **kwargs):
906966
Axes.set_yscale(self, *args, **kwargs)

0 commit comments

Comments
 (0)