2

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)


ascendants
  • 919
  • 6
  • 18

1 Answers1

2

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:

result of above code

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.

Thomas Kühn
  • 8,046
  • 3
  • 33
  • 52
  • Thanks for your insight about the plotting coordinates, this helped resolve the issue. I'm curious -- is this odd coord system documented by Matplotlib? Seems like it's fine and under the hood for plotting points, but would be problematic for adding anything else. And the radii details make sense. I knew that the outer radius is passed to Wedge, but the problem was caused by the width not being transformed. This all solved my issues, thank you! – ascendants Feb 02 '19 at 20:27
  • Actually, this only fixed the issue when showing the entire polar plot; there is still a problem when plotting a wedge onto a sector plot [link](https://i.stack.imgur.com/RY8j4.png). Clearly the wedge is drawn at the same "global" location in both plots. What scaling must I do to display the wedge correctly in the sector? – ascendants Feb 03 '19 at 01:22
  • @ascendants How do you produce the sector plot? – Thomas Kühn Feb 03 '19 at 06:39
  • I do it using ax.set_thetamin(), set_thetamax(), and set_rorigin(): ax.set_thetamin(np.rad2deg(minTheta)); ax.set_thetamax(np.rad2deg(maxTheta)); ax.set_rorigin(-minZ) – ascendants Feb 06 '19 at 17:31
  • 1
    @ascendants I finally got around to look into this. Please see the edit to my answer. – Thomas Kühn Feb 15 '19 at 10:45
  • 1
    Ah I figured it was a deeper problem than what I first thought. I adapted your code and it finally works as I imagined! Thank you for making all this effort to compile this great answer. – ascendants Feb 22 '19 at 21:04