Getting Labels on top of Bar in Polar/Radial Bar Chart in Matplotlib, Python3

回眸只為那壹抹淺笑 提交于 2019-11-29 22:43:51

问题


I want to create a radial bar chart. I have the following Python3 code:

lObjectsALLcnts = [1, 1, 1, 2, 2, 3, 5, 14, 15, 20, 32, 33, 51, 1, 1, 2, 2, 3, 3, 3, 3, 3, 4, 6, 7, 7, 10, 10, 14, 14, 14, 17, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 5, 5, 6, 14, 14, 27, 27, 1, 1, 2, 3, 4, 4, 5]`

lObjectsALLlbls = ['DuctPipe', 'Column', 'Protrusion', 'Tree', 'Pole', 'Bar', 'Undefined', 'EarthingConductor', 'Grooves', 'UtilityPipe', 'Cables', 'RainPipe', 'Moulding', 'Intrusion', 'PowerPlug', 'UtilityBox', 'Balcony', 'Lighting', 'Lock', 'Doorbell', 'Alarm', 'LetterBox', 'Grate', 'Undefined', 'CableBox', 'Canopy', 'Vent', 'PowerBox', 'UtilityHole', 'Recess', 'Protrusion', 'Shutter', 'Handrail', 'Lock', 'Mirror', 'SecuritySpike', 'Bench', 'Intrusion', 'Picture', 'Showcase', 'Camera', 'Undefined', 'Stair', 'Protrusion', 'Alarm', 'Graffiti', 'Lighting', 'Ornaments', 'SecurityBar', 'Grate', 'Vent', 'Lighting', 'UtilityHole', 'Intrusion', 'Undefined', 'Protrusion']

iN = len(lObjectsALLcnts)
arrCnts = np.array(lObjectsALLcnts)

theta=np.arange(0,2*np.pi,2*np.pi/iN)
width = (2*np.pi)/iN *0.9

fig = plt.figure(figsize=(8, 8))
ax = fig.add_axes([0.1, 0.1, 0.75, 0.75], polar=True)
bars = ax.bar(theta, arrCnts, width=width, bottom=50)
ax.set_xticks(theta)
plt.axis('off')

which creates the following image:

radialbartchart_nolabels

After creating this I would like to add labels, but I'm having a bit of troubles finding the right coordinates. The labels should be rotated along the directions of the bars.

The best I've come up with is adding the following code:

rotations = [np.degrees(i) for i in theta]
for i in rotations: i = int(i)
for x, bar, rotation, label in zip(theta, bars, rotations, lObjectsALLlbls):
     height = bar.get_height() + 50
     ax.text(x + bar.get_width()/2, height, label, ha='center', va='bottom', rotation=rotation)

which creates the following:

radialbarchart_wlabels

Can some help me with finding the right coordinates for the labels? I've been looking in to answers like Adding value labels on a matplotlib bar chart and translating it to the polar bar chart. But with no success.

Thanks in advance,

A long time reader on StackOverflow, but for the first time I couldn't find an answer.


回答1:


The problem you run into is that the text bounding box is expanded to host the complete rotated text, but that box itself is still defined in cartesian coordinates. The picture below shows two texts with horizontalalignment "left" and vertical alignment "bottom"; the problem is that the rotated text has its bounding box edge much further away from the text.

What you want is rather to have the text rotate about an edge point of its own surrounding as below.

This can be achieved using the rotation_mode="anchor" argument to matplotlib.text.Text, which steers exactly the above functionality.

ax.text(..., rotation_mode="anchor")

In this example:

from matplotlib import pyplot as plt
import numpy as np

lObjectsALLcnts = [1, 1, 1, 2, 2, 3, 5, 14, 15, 20, 32, 33, 51, 1, 1, 2, 2, 3, 3, 3, 3, 
                   3, 4, 6, 7, 7, 10, 10, 14, 14, 14, 17, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 
                   5, 5, 6, 14, 14, 27, 27, 1, 1, 2, 3, 4, 4, 5]

lObjectsALLlbls = ['DuctPipe', 'Column', 'Protrusion', 'Tree', 'Pole', 'Bar', 'Undefined', 
                   'EarthingConductor', 'Grooves', 'UtilityPipe', 'Cables', 'RainPipe', 'Moulding', 
                   'Intrusion', 'PowerPlug', 'UtilityBox', 'Balcony', 'Lighting', 'Lock', 'Doorbell', 
                   'Alarm', 'LetterBox', 'Grate', 'Undefined', 'CableBox', 'Canopy', 'Vent', 'PowerBox', 
                   'UtilityHole', 'Recess', 'Protrusion', 'Shutter', 'Handrail', 'Lock', 'Mirror', 
                   'SecuritySpike', 'Bench', 'Intrusion', 'Picture', 'Showcase', 'Camera', 
                   'Undefined', 'Stair', 'Protrusion', 'Alarm', 'Graffiti', 'Lighting', 'Ornaments', 
                   'SecurityBar', 
                   'Grate', 'Vent', 'Lighting', 'UtilityHole', 'Intrusion', 'Undefined', 'Protrusion']

iN = len(lObjectsALLcnts)
arrCnts = np.array(lObjectsALLcnts)

theta=np.arange(0,2*np.pi,2*np.pi/iN)
width = (2*np.pi)/iN *0.9
bottom = 50

fig = plt.figure(figsize=(8,8))
ax = fig.add_axes([0.1, 0.1, 0.75, 0.75], polar=True)
bars = ax.bar(theta, arrCnts, width=width, bottom=bottom)

plt.axis('off')

rotations = np.rad2deg(theta)
for x, bar, rotation, label in zip(theta, bars, rotations, lObjectsALLlbls):
    lab = ax.text(x,bottom+bar.get_height() , label, 
             ha='left', va='center', rotation=rotation, rotation_mode="anchor",)   
plt.show()

Note that this uses the given 50 units of bottom spacing. You may increase this number a bit to have more spacing between bars and text.


The below initial version of this answer is somehow outdated. I will keep it here for reference.

The problem you run into is that the text bounding box is expanded to host the complete rotated text, but that box itself is still defined in cartesian coordinates. The picture below shows two texts with horizontalalignment "left" and vertical alignment "bottom"; the problem is that the rotated text has its bounding box edge much further away from the text.

An easy solution may be to define the horizontal and vertical alignment as "center", such the pivot of the text stays the same independent of its rotation.

The problem would then be to get a good estimate for the distance between the center of the text and the bar's top.

One could take half the number of letters in the text and multiply it with some factor. This would need to be found by trial and error.

bottom = 50
rotations = np.rad2deg(theta)
y0,y1 = ax.get_ylim()

for x, bar, rotation, label in zip(theta, bars, rotations, lObjectsALLlbls):
     offset = (bottom+bar.get_height())/(y1-y0)
     h =offset + len(label)/2.*0.032
     lab = ax.text(x, h, label, transform=ax.get_xaxis_transform(), 
             ha='center', va='center')
     lab.set_rotation(rotation)

You could also try to find out how large the rendered text really is and use this information to find out the coordinates,

bottom = 50
rotations = np.rad2deg(theta)
y0,y1 = ax.get_ylim()

for x, bar, rotation, label in zip(theta, bars, rotations, lObjectsALLlbls):
     offset = (bottom+bar.get_height())/(y1-y0)
     lab = ax.text(0, 0, label, transform=None, 
             ha='center', va='center')
     renderer = ax.figure.canvas.get_renderer()
     bbox = lab.get_window_extent(renderer=renderer)
     invb = ax.transData.inverted().transform([[0,0],[bbox.width,0] ])
     lab.set_position((x,offset+(invb[1][0]-invb[0][0])/2.*2.7 ) )
     lab.set_transform(ax.get_xaxis_transform())
     lab.set_rotation(rotation)

Complete code for reproduction:

import numpy as np
import matplotlib.pyplot as plt

lObjectsALLcnts = [1, 1, 1, 2, 2, 3, 5, 14, 15, 20, 32, 33, 51, 1, 1, 2, 2, 3, 3, 3, 3, 
               3, 4, 6, 7, 7, 10, 10, 14, 14, 14, 17, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 
               5, 5, 6, 14, 14, 27, 27, 1, 1, 2, 3, 4, 4, 5]

lObjectsALLlbls = ['DuctPipe', 'Column', 'Protrusion', 'Tree', 'Pole', 'Bar', 'Undefined', 
               'EarthingConductor', 'Grooves', 'UtilityPipe', 'Cables', 'RainPipe', 'Moulding', 
               'Intrusion', 'PowerPlug', 'UtilityBox', 'Balcony', 'Lighting', 'Lock', 'Doorbell', 
               'Alarm', 'LetterBox', 'Grate', 'Undefined', 'CableBox', 'Canopy', 'Vent', 'PowerBox', 
               'UtilityHole', 'Recess', 'Protrusion', 'Shutter', 'Handrail', 'Lock', 'Mirror', 
               'SecuritySpike', 'Bench', 'Intrusion', 'Picture', 'Showcase', 'Camera', 
               'Undefined', 'Stair', 'Protrusion', 'Alarm', 'Graffiti', 'Lighting', 'Ornaments', 
               'SecurityBar', 
               'Grate', 'Vent', 'Lighting', 'UtilityHole', 'Intrusion', 'Undefined', 'Protrusion']

iN = len(lObjectsALLcnts)
arrCnts = np.array(lObjectsALLcnts)

theta=np.arange(0,2*np.pi,2*np.pi/iN)
width = (2*np.pi)/iN *0.9
bottom = 50

fig = plt.figure(figsize=(8,8))
ax = fig.add_axes([0.1, 0.1, 0.75, 0.75], polar=True)
bars = ax.bar(theta, arrCnts, width=width, bottom=bottom)

plt.axis('off')

rotations = np.rad2deg(theta)
y0,y1 = ax.get_ylim()

for x, bar, rotation, label in zip(theta, bars, rotations, lObjectsALLlbls):
 offset = (bottom+bar.get_height())/(y1-y0)
 lab = ax.text(0, 0, label, transform=None, 
         ha='center', va='center')
 renderer = ax.figure.canvas.get_renderer()
 bbox = lab.get_window_extent(renderer=renderer)
 invb = ax.transData.inverted().transform([[0,0],[bbox.width,0] ])
 lab.set_position((x,offset+(invb[1][0]-invb[0][0])/2.*2.7 ) )
 lab.set_transform(ax.get_xaxis_transform())
 lab.set_rotation(rotation)

 
plt.show()

Unfortunately there is again some strange factor 2.7 involved. Even more unfornate is that in this case I have absolutely no idea why it must be there. But the result may still be good enough to work with.

One could also use a solution from ths question: Align arbitrarily rotated text annotations relative to the text, not the bounding box



来源:https://stackoverflow.com/questions/46874689/getting-labels-on-top-of-bar-in-polar-radial-bar-chart-in-matplotlib-python3

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!