How to add a wedge sector onto a polar Matplotlib plot

|▌冷眼眸甩不掉的悲伤 提交于 2021-02-08 04:41:13

问题


I wish to add a wedge outlining a group of polar data using Python's Matplotlib. I have tried using the Wedge patch artist unsuccessfully, due to unknown reasons. I wish to either understand these unknowns, or find an alternative to the patch artist approach.

The primary issue is that the Wedge patch is not displaying as I am expecting it to. Given my code, I am expecting it to be oriented at an angle, and span a range of ~0.05 in radius. This places it within the sector plot here: 1

But this wedge has different dimensions and location than what I'm expecting. It also is shifted when viewing a zoomed-out plot: 2

The wedge has approximately the correct angular range (about ~25-27 degrees), but it starts at the wrong radius (should be ~0.4), and is the wrong width (should be ~0.05). Why is this, and how can I draw a wedge with these desired dimensions?

I have already viewed and adapted code from similar questions (see, e.g., Python: Add a Ring Sector or a Wedge to a Polar Plot ).

Here is an adaptation of my main code, with sample data included.


import numpy as np
import matplotlib.pyplot as plt

from matplotlib.patches import Wedge


##Enter data

thetaRad = np.array([0.455, 0.456, 0.455, 0.456, 0.46 , 0.459, 0.461, 0.461, 0.453,
       0.459, 0.46 , 0.46 , 0.46 , 0.451, 0.46 , 0.457, 0.45 , 0.451,
       0.45 , 0.45 , 0.451, 0.452, 0.461, 0.459, 0.451, 0.455, 0.454,
       0.457, 0.459, 0.451, 0.46 , 0.453, 0.46 , 0.452, 0.452, 0.45 ,
       0.453, 0.452, 0.452, 0.456, 0.45 , 0.458, 0.461, 0.457, 0.45 ,
       0.453, 0.459, 0.459, 0.455, 0.456, 0.457, 0.457, 0.454, 0.453,
       0.455, 0.456, 0.459, 0.455, 0.453, 0.455, 0.454, 0.459, 0.457,
       0.454, 0.46 , 0.458, 0.459, 0.457, 0.451, 0.45 , 0.455, 0.461,
       0.455, 0.458, 0.456, 0.449, 0.459, 0.453, 0.458, 0.457, 0.456,
       0.45 , 0.459, 0.458, 0.453, 0.452, 0.459, 0.454, 0.455, 0.452,
       0.453, 0.451, 0.453, 0.461, 0.452, 0.458, 0.449, 0.461, 0.459,
       0.452, 0.458, 0.455, 0.452, 0.451, 0.457, 0.457, 0.457, 0.457,
       0.456, 0.456, 0.451, 0.451, 0.452, 0.459, 0.45 , 0.453, 0.45 ,
       0.449, 0.453, 0.455, 0.457])

Zs = np.array([0.052, 0.052, 0.057, 0.058, 0.058, 0.058, 0.058, 0.058, 0.059,
       0.059, 0.059, 0.059, 0.06 , 0.06 , 0.06 , 0.06 , 0.064, 0.134,
       0.134, 0.134, 0.134, 0.135, 0.135, 0.135, 0.135, 0.135, 0.135,
       0.135, 0.135, 0.135, 0.135, 0.135, 0.135, 0.136, 0.136, 0.136,
       0.136, 0.136, 0.136, 0.137, 0.309, 0.311, 0.32 , 0.328, 0.352,
       0.379, 0.381, 0.381, 0.382, 0.382, 0.383, 0.383, 0.386, 0.387,
       0.39 , 0.392, 0.392, 0.392, 0.392, 0.393, 0.393, 0.394, 0.394,
       0.394, 0.394, 0.394, 0.394, 0.395, 0.395, 0.396, 0.422, 0.426,
       0.48 , 0.482, 0.483, 0.483, 0.484, 0.487, 0.487, 0.489, 0.489,
       0.49 , 0.49 , 0.491, 0.491, 0.491, 0.491, 0.492, 0.492, 0.496,
       0.497, 0.498, 0.5  , 0.505, 0.764, 0.767, 0.771, 0.771, 0.777,
       0.833, 0.844, 0.855, 0.858, 0.863, 0.866, 0.868, 0.869, 0.87 ,
       0.871, 0.872, 0.875, 0.994, 0.995, 0.996, 1.002, 1.004, 1.01 ,
       1.01 , 1.011, 1.475, 1.667])


maxZ = 0.55
minZ = 0.28


##Prepare plot

fig = plt.figure()
color = 'k'
m = 'o'
size = 1

ax = fig.add_subplot(111, projection='polar')
plt.scatter(thetaRad,Zs, c=color, marker=m, s = size)

ax.set_rmax(maxZ)
ax.set_rmin(minZ)

#set theta limits to be scaled from the dataset
minTheta = 0.95*min(thetaRad)
maxTheta = 1.05*max(thetaRad)

#uncomment these for the partial sector plot:
#ax.set_thetamin(np.rad2deg(minTheta))
#ax.set_thetamax(np.rad2deg(maxTheta))
#ax.set_rorigin(-minZ)

ticks = np.linspace(minTheta, maxTheta, 4)
ax.set_xticks(ticks)


##Add a wedge

#define the wedge's width and range
window = np.array([0.35,0.40])
dTheta = np.deg2rad(0.5)
wedgeRange = [minTheta+dTheta, maxTheta-dTheta]
wedgeRange = np.rad2deg(wedgeRange)

r = window[1]
width = window[1]-window[0]
width = width

#apparently, plt's polar plot is silently centered at (0.5,0.5) instead of the
#origin, so set this:
center = (0.5,0.5)

wedge = Wedge(center, r, wedgeRange[0],wedgeRange[1],width=width, transform=ax.transAxes, linestyle='--', fill=False, color='red')
ax.add_artist(wedge)



回答1:


This turned out to be much more complicated that I at first anticipated. The main problem here is that the coordinates and angles given to Wedge are in axes coordinates, while what is really wanted is a Wedge in data coordinates. Especially the angles are a little bit hard to get right.

The solution I found is to convert the corner points of the wedge into Axes coordinates and then use these points to compute the center, radii, and angles of the wedge using linear algebra. There probably is a way to do this straight with data coordinates, but at least this works. I found help in the matplotlib transformation tutorial and in some other SO answers:

  • this answer for how to compute the intersect of two lines
  • this answer for how to compute the angle between two lines
  • this answer for how to solve a problem with transformations in equal-aspect Axes
  • this answer for how to fix the r-limits of a polar axes instance

To make the solution a little bit easier to explain, I changed the wedge coordinates in my example and added some numbered annotations for the geometrical points that I used in the calculations. Here is the code:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Wedge

def perp( a ) :
    ##from https://stackoverflow.com/a/3252222/2454357
    b = np.empty_like(a)
    b[0] = -a[1]
    b[1] = a[0]
    return b


def seq_intersect(a1,a2, b1,b2) :
    ##from https://stackoverflow.com/a/3252222/2454357
    da = a2-a1
    db = b2-b1
    dp = a1-b1
    dap = perp(da)
    denom = np.dot( dap, db)
    num = np.dot( dap, dp )
    return (num / denom.astype(float))*db + b1

def angle(a1, a2, b1, b2):
    ##from https://stackoverflow.com/a/16544330/2454357
    x1, y1 = a2-a1
    x2, y2 = b2-b1
    dot = x1*x2 + y1*y2      # dot product between [x1, y1] and [x2, y2]
    det = x1*y2 - y1*x2      # determinant
    return np.arctan2(det, dot)  # atan2(y, x) or atan2(sin, cos)


def draw_wedge(
    ax, r_min = 0.3, r_max = 0.5, t_min = np.pi/4, t_max = 3*np.pi/4
    ):

    ##some data
    R = np.random.rand(100)*(r_max-r_min)+r_min
    T = np.random.rand(100)*(t_max-t_min)+t_min
    ax.scatter(T,R)

    ##compute the corner points of the wedge:
    axtmin = 0

    rs = np.array([r_min,  r_max,  r_min, r_max, r_min, r_max])
    ts = np.array([axtmin, axtmin, t_min, t_min, t_max, t_max])

    ##display them in a scatter plot
    ax.scatter(ts, rs, color='r', marker='x', lw=5)

    ##from https://matplotlib.org/users/transforms_tutorial.html
    trans = ax.transData + ax.transAxes.inverted()

    ##convert to figure cordinates, for a starter
    xax, yax = trans.transform([(t,r) for t,r in zip(ts, rs)]).T

    for i,(x,y) in enumerate(zip(xax, yax)):
        ax.annotate(
            str(i), (x,y), xytext = (x+0.1, y), xycoords = 'axes fraction',
            arrowprops = dict(
                width=2,

            ),
        )


    ##compute the angles of the wedge:
    tstart = np.rad2deg(angle(*np.array((xax[[0,1,2,3]],yax[[0,1,2,3]])).T))
    tend = np.rad2deg(angle(*np.array((xax[[0,1,4,5]],yax[[0,1,4,5]])).T))

    ##the center is where the two wedge sides cross (maybe outside the axes)
    center=seq_intersect(*np.array((xax[[2,3,4,5]],yax[[2,3,4,5]])).T)

    ##compute the inner and outer radii of the wedge:
    rinner = np.sqrt((xax[1]-center[0])**2+(yax[1]-center[1])**2)
    router = np.sqrt((xax[2]-center[0])**2+(yax[2]-center[1])**2)

    wedge = Wedge(center,
                  router, tstart, tend,
                  width=router-rinner,
                  #0.6,tstart,tend,0.3,
                  transform=ax.transAxes, linestyle='--', lw=3,
                  fill=False, color='red')
    ax.add_artist(wedge)


fig = plt.figure(figsize=(8,4))

ax1 = fig.add_subplot(121, projection='polar')
ax2 = fig.add_subplot(122, projection='polar')

##reducing the displayed theta and r ranges in second axes:
ax2.set_thetamin(10)
ax2.set_thetamax(40)

## ax.set_rmax() does not work as one would expect -- use ax.set_ylim() instead
## from https://stackoverflow.com/a/9231553/2454357
ax2.set_ylim([0.2,0.8])
ax2.set_rorigin(-0.2)

#from https://stackoverflow.com/a/41823326/2454357
fig.canvas.draw()

draw_wedge(ax1)
draw_wedge(ax2, t_min=np.deg2rad(15), t_max=np.deg2rad(30))

plt.show()

And the image that it produces:

Explanation:

In the code I define 6 geometrical points: the 4 corners of the wedge and two points on the theta=0 line that correspond to the inner and outer radii of the wedge. I then transform these points from data to axes coordinates using the transform ax.transData+ax.transAxes.inverted(). Now, in axes coordinates I use these points to compute the center of the wedge (the intersect of the left and right sides of the wedge; points 2,3,4, and 5) and the angles between the theta=0 line and the sides of the wedge (points 0,1,2,3 and 0,1,4,5, respectively). The two radii can be computed as the Euclidean distance between the wedge center and, say, points 2 and 3. With these numbers the wedge can finally be constructed.

Note that this solution is not robust agains all figure and axes manipulations. In particular changing axes limits or aspect ratios after adding the wedge will misplace it. Resizing of the Figure is ok and tested. Hope this helps.

Old Answer:

This is a bit funny, but apparently the radius argument is not relative to the data of the Axes. You can check this by adding a wedge of radius 0.5, which, together with center=(0.5,0.5), will produce a wedge that spans the entire data range. You can define a function to transform the wedge radii from data coordinates to these coordinates:

def transform_radius(r, rmin, rmax):
    return (r-rmin)/(rmax-rmin)*0.5

Here rmin and rmax are the minimum and maximum radii of the Axes, respectively. Another issue is the confusion with how the partial wedge is drawn. According to the documentation:

If width is given, then a partial wedge is drawn from inner radius r-width to outer radius r.

So in your case the radius you pass to Wedge should be the outer, not the inner, radius. Putting it all together, this should display the wedge correctly:

r_inner = transform_radius(r, minZ, maxZ)
r_outer = transform_radius(r+width, minZ, maxZ)
wedge = Wedge(    
    center,
    r_outer,
    wedgeRange[0],wedgeRange[1],
    width=r_outer-r_inner,
    transform=ax.transAxes, linestyle='--',
    fill=False, color='red'
)
ax.add_artist(wedge)

Please let me know if I misunderstood something.



来源:https://stackoverflow.com/questions/54395307/how-to-add-a-wedge-sector-onto-a-polar-matplotlib-plot

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