TL/DR: How to use Wedge() in polar coordinates?
I'm generating a 2D histogram plot in polar coordinates (r, theta). At various values of r there can be different numbers of theta values (to preserve equal area sized bins). To draw the color coded bins I'm currently using pcolormesh() calls for each radial ring. This works ok, but near the center of the plot where there may be only 3 bins (each 120 degrees "wide" in theta space), pcolormesh() draws triangles that don't "sweep" out full arc (just connecting the two outer arc points with a straight line).
I've found a workaround using ax.bar() call, one for each radial ring and passing in arrays of theta values (each bin rendering as an individual bar). But when doing 90 rings with 3 to 360 theta bins in each, it's incredibly slow (minutes).
I tried using Wedge() patches, but can't get them to render correctly in the polar projection. Here is sample code showing both approaches:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Wedge
from matplotlib.collections import PatchCollection
# Theta coordinates in degrees
theta1=45
theta2=80
# Radius coordinates
r1 = 0.4
r2 = 0.5
# Plot using bar()
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
theta_mid = np.deg2rad((theta1 + theta2)/2)
theta_width = np.deg2rad(theta2 - theta1)
height = r2 - r1
ax.bar(x=theta_mid, height = height, width=theta_width, bottom=r1)
ax.set_rlim(0, 1)
plt.savefig('bar.png')
# Plot using Wedge()
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
patches = []
patches.append( Wedge(center=(0, 0), r = r1, theta1=theta1, theta2=theta2, width = r2-r1, color='blue'))
p = PatchCollection(patches)
ax.add_collection(p)
ax.set_rlim(0, 1)
plt.savefig('wedge.png')
The outputs of each are:
Bar
Wedge
I've tried using radians for the wedge (because polar plots usually want their angle values in radians). That didn't help.
Am I missing something in how I'm using the Wedge? If I add thousands of Wedges to my Patch collection should I have any expectation it will be faster than bar()?
Thinking this was an actual bug, I opened this issue https://github.com/matplotlib/matplotlib/issues/22717 on matplotlib where one of the maintainers nicely pointed out that I should be using Rectangle() instead of Wedge().
The solution they provided is
from matplotlib.patches import Rectangle
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
p = PatchCollection([Rectangle((np.deg2rad(theta1), r1), theta_width, height, color='blue')])
ax.add_collection(p)
ax.set_rlim(0, 1)
plt.savefig('wedge.png')
Related
I would like to annotate a scatterplot with images corresponding to each datapoint. With standard parameters the images end up clashing with each other and other important features such as legend axis, etc. Thus, I would like the images to form a circle or a rectangle around the main scatter plot.
My code looks like this for now and I am struggling to modify it to organise the images around the center point of the plot.
import matplotlib.cbook as cbook
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import seaborn as sns
#Generate n points around a 2d circle
def generate_circle_points(n, centre_x, center_y, radius=1):
"""Generate n points around a circle.
Args:
n (int): Number of points to generate.
centre_x (float): x-coordinate of circle centre.
center_y (float): y-coordinate of circle centre.
radius (float): Radius of circle.
Returns:
list: List of points.
"""
points = []
for i in range(n):
angle = 2 * np.pi * i / n
x = centre_x + radius * np.cos(angle)
y = center_y + radius * np.sin(angle)
points.append([x, y])
return points
fig, ax = plt.subplots(1, 1, figsize=(7.5, 7.5))
data = pd.DataFrame(data={'x': np.random.uniform(0.5, 2.5, 20),
'y': np.random.uniform(10000, 50000, 20)})
with cbook.get_sample_data('grace_hopper.jpg') as image_file:
image = plt.imread(image_file)
# Set logarithmic scale for x and y axis
ax.set(xscale="log", yscale='log')
# Add grid
ax.grid(True, which='major', ls="--", c='gray')
coordianates = generate_circle_points(n=len(data),
centre_x=0, center_y=0, radius=10)
# Plot the scatter plot
scatter = sns.scatterplot(data=data, x='x', y='y', ax=ax)
for index, row in data.iterrows():
imagebox = OffsetImage(image, zoom=0.05)
imagebox.image.axes = ax
xy = np.array([row['x'], row['y']])
xybox = np.array(coordianates[index])
ab = AnnotationBbox(imagebox, xy,
xycoords='data',
boxcoords="offset points",
xybox=xybox,
pad=0)
ax.add_artist(ab)
for the moment the output looks like this:enter image description here
Ideally I would like the output to look to something like this:
enter image description here
Many thanks in advance for your help
Not an answer but a long comment:
You can control the location of the arrows, but sometimes it is easier to export figures as SVGs and edit them in Adobe Illustrator or Inkscape.
R has a dodge argument which is really nice, but even then is not always perfect. Solutions in Python exist but are laborious.
The major issue is that this needs to be done last as alternations to the plot would make it problematic. A few points need mentioning.
Your figures will have to have a fixed size (57mm / 121mm / 184mm for Science, 83mm / 171mm for RSC, 83mm / 178mm for ACS etc.), if you need to scale the figure in Illustrator keep note of the scaling factor, adding it as a textbox outside of the canvas —as the underlying plot will need to be replaced at least once due to Murphy's law. Exporting at the right size the SVG is ideal. Sounds silly, but it helps. Likewise, make sure the font size does not go under the minimum spec (7-9 points).
Consider the following data, which is defined in polar space in theta, r, and is plotted twice; once in the the orthogonal theta-r phase space, and one in cartesian space after an inverse transformation from polar coordinates to x-y (i.e. what matplotlib's projection='polar' does):
import matplotlib.pyplot as plt
import numpy as np
theta = np.linspace(0, 2*np.pi, 50)
r = np.linspace(0, 1, 50)
THETA, R = np.meshgrid(theta, r)
Z = np.sin(R*np.pi) * np.sin(THETA+np.pi/2)
fig = plt.figure()
axpol = fig.add_subplot(121)
axcart = fig.add_subplot(122, projection='polar')
axcart.contourf(THETA, R, Z, levels=10)
axpol.contourf(THETA, R, Z, levels=10)
axcart.set_title('cartesian space')
axpol.set_title('polar space')
axpol.set_xlim([0, 2*np.pi])
axpol.set_xlabel('r')
axpol.set_ylabel('theta')
plt.show()
This produces:
(NOTE: Oops, the axis labels in the polar plots (left side) should be swapped in each of the images below)
Now, if we shift the theta array by pi:
theta = np.linspace(np.pi, 3*np.pi, 50)
and rerun the above, we see
Notice that the data plotted in the projected polar space successfully wraps the data at theta > 2*np.pi back to the beginning of the angular domain (since this is defined in the projections inverse transformation), such that it appears unchanged. In polar space, this does not happen.
Of course, this is expected; this axis has no associated transformation, and thus does know know how to wrap the data, or that it even should.
My question is, how can I enable this behavior, without having to shift the coordinates and data manually? That is, is there a way to have the axis on the left of the figure above inherit the polar transformation, but not the projection?
I would prefer to do this without defining my own transformation or projection objects. I thought there should be a way to inherit this small piece of the polar transformation, without doing the "full" transformation to Cartesian x,y.
I want to use in matplotlib.patches.Arc the clip_path parameter, but do not succeed.
Next is just an example, where I want to see not the complete orange arc but only the partial orange arc between y-axis and the red circle by using the clip_path parameter, but do no understand how to define the clip_path parameters. Thanks.
import math as m
import matplotlib.pyplot as plt
import matplotlib.patches as pat
plt.figure(figsize=(10,10),dpi=300)
ag=10
plt.axis([-ag,ag,-ag,ag])
plt.grid(True)
circle1 = plt.Circle((0, 2.5), 7, color='r',fill=False)
plt.gcf().gca().add_artist(circle1)
myarc=pat.Arc((0,0),25,18,angle=0,theta1=0,theta2=355,color="orange")
plt.gcf().gca().add_artist(myarc)
plt.savefig("myarc.png")
plt.show()
This is what I got:
Just a further remark: With next modification of theta1 and theta2 angle I get what I need, but for this the two intersections need to be determined first. My intention is to avoid these calculations and just draw an ellipse and defining two clipping paths (the red circle and the y-axis).
myarc=pat.Arc((0,0),25,18,angle=0,theta1=110,theta2=152,color="orange")
To clip the arc by the circle, you can use myarc.set_clip_path(circle1). It is important that both the arc and the circle are previously added to the plot (ax.add_artist()). Note that clipping by the borders of the axes happens automatically.
To create more complicated clipping, the shapely is probably handier.
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
fig, ax = plt.subplots(figsize=(10, 6))
circle1 = plt.Circle((0, 2.5), 7, color='r', fill=False)
ax.add_artist(circle1)
myarc = mpatches.Arc((0, 0), 25, 18, angle=0, theta1=0, theta2=355, color="orange", lw=5)
ax.add_artist(myarc)
myarc.set_clip_path(circle1)
ag = 10
ax.set_xlim(-ag, ag)
ax.set_ylim(-ag, ag)
plt.grid(True)
ax.set_aspect('equal') # set the aspect ratio so circles look like circles
plt.show()
By using the steps of answer (1) I got the wanted result without the need to calculate all the intersections. Steps:
Defining and Plotting series of curves
Defining clipping areas by using clip_path option (e.g. circles or shaping an area by concatenating 1D-arrays through mathematical function results)
Using clip_path to get rid of unwanted portion of curves
# Import python Modules
import math as m
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Polygon
## constants for the circles
Loc=37 # in degree
StepAl=3 # in degree
StepAz=10 # in degree
rAequ=6.3 # Radius
rWkSt=9.6 # Radius
Ze=3.14 # Distance
## red AlCircles, in total 31
AlCircle=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
i=0
while i<=30:
AlCircle[i]=[0,0,0,0]
i=i+1
# Calculating the parameters of the AlCircles
i=0
while i<=30:
AlCircle[i][0]=rAequ*m.tan((-Loc+i*StepAl)/2/180*m.pi) # lower y-Value
AlCircle[i][1]=rAequ*m.tan((-Loc+180-i*StepAl)/2/180*m.pi) # upper y-Value
AlCircle[i][2]=(AlCircle[i][1]-AlCircle[i][0])/2 # Radius
AlCircle[i][3]=AlCircle[i][0]+AlCircle[i][2] # Center
i=i+1
## green AzCircles, in total 18
DZ=rAequ/m.cos(Loc/180*m.pi) # Distance
AzCircle=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
i=0
while i<=17:
AzCircle[i]=[0,0]
i=i+1
# Calculating the parameters of the AzCircles
i=1
while i<=17:
AzCircle[i][0]=DZ*m.tan((-90+i*StepAz)/180*m.pi) # distance Center to y-Axis
AzCircle[i][1]=rAequ/m.cos(Loc/180*m.pi)/m.cos((-90+i*StepAz)/180*m.pi) # Radius of AzCircles
i=i+1
### Generating Plots
plt.figure(figsize=(10,10),dpi=100)
ag=rWkSt
plt.axis([-ag,ag,-ag,ag])
plt.grid(True)
# Plotting blue Circle
circle0=plt.Circle((0,0),rWkSt,color='b',fill=False)
plt.gcf().gca().add_artist(circle0)
# Plotting red AlCircles
i=0
while i<=30:
# defining Cliparea1
myCliparea1=plt.Circle((0,0),rWkSt,color="b",ls="dotted",fill=False)
plt.gcf().gca().add_artist(myCliparea1)
# calculating AlCircles and plotting
circle1=plt.Circle((0,AlCircle[i][3]),AlCircle[i][2],color='r',fill=False)
plt.gcf().gca().add_artist(circle1)
circle1.set_clip_path(myCliparea1) # performing clipping
i=i+1
# Plotting green AzCircles
i=1
while i<=17: # nur bis 17
xA=9.072582 # precalculated Intersection for f1(x) and f2(x)
# f1(x) for lower clipping area border line
x1=np.arange(-xA,+xA,0.1)
y1=(-1)*np.sqrt(AlCircle[0][2]**2-x1**2)+AlCircle[0][3]
# f2(x) for upper clipping area border line
x2=np.arange(xA,-xA,-0.1)
y2=(+1)*np.sqrt(rWkSt**2-x2**2)
# building clipping area
x3=np.concatenate((x1,x2))
y3=np.concatenate((y1,y2))
poly = Polygon(np.column_stack([x3, y3]), animated=True, color="aqua", fill=False)
plt.gcf().gca().add_artist(poly) # plotting of clipping area
# calculating AzCircles and plotting
circle2=plt.Circle((-AzCircle[i][0],Ze-DZ),AzCircle[i][1],color='g',fill=False)
plt.gcf().gca().add_artist(circle2)
circle2.set_clip_path(poly) # performing clipping
i=i+1
plt.savefig("myPlot.png")
plt.show()
myPlot
I'm trying to adapt the following resources to this question:
Python conversion between coordinates
https://matplotlib.org/gallery/pie_and_polar_charts/polar_scatter.html
I can't seem to get the coordinates to transfer the dendrogram shape over to polar coordinates.
Does anyone know how to do this? I know there is an implementation in networkx but that requires building a graph and then using pygraphviz backend to get the positions.
Is there a way to convert dendrogram cartesian coordinates to polar coordinates with matplotlib and numpy?
import requests
from ast import literal_eval
import matplotlib.pyplot as plt
import numpy as np
def read_url(url):
r = requests.get(url)
return r.text
def cartesian_to_polar(x, y):
rho = np.sqrt(x**2 + y**2)
phi = np.arctan2(y, x)
return(rho, phi)
def plot_dendrogram(icoord,dcoord,figsize, polar=False):
if polar:
icoord, dcoord = cartesian_to_polar(icoord, dcoord)
with plt.style.context("seaborn-white"):
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111, polar=polar)
for xs, ys in zip(icoord, dcoord):
ax.plot(xs,ys, color="black")
ax.set_title(f"Polar= {polar}", fontsize=15)
# Load the dendrogram data
string_data = read_url("https://pastebin.com/raw/f953qgdr").replace("\r","").replace("\n","").replace("\u200b\u200b","")
# Convert it to a dictionary (a subset of the output from scipy.hierarchy.dendrogram)
dendrogram_data = literal_eval(string_data)
icoord = np.asarray(dendrogram_data["icoord"], dtype=float)
dcoord = np.asarray(dendrogram_data["dcoord"], dtype=float)
# Plot the cartesian version
plot_dendrogram(icoord,dcoord, figsize=(8,3), polar=False)
# Plot the polar version
plot_dendrogram(icoord,dcoord, figsize=(5,5), polar=True)
I just tried this and it's closer but still not correct:
import matplotlib.transforms as mtransforms
with plt.style.context("seaborn-white"):
fig, ax = plt.subplots(figsize=(5,5))
for xs, ys in zip(icoord, dcoord):
ax.plot(xs,ys, color="black",transform=trans_offset)
ax_polar = plt.subplot(111, projection='polar')
trans_offset = mtransforms.offset_copy(ax_polar.transData, fig=fig)
for xs, ys in zip(icoord, dcoord):
ax_polar.plot(xs,ys, color="black",transform=trans_offset)
You can make the "root" of the tree start in the middle and have the leaves outside. You also have to add more points to the "bar" part for it to look nice and round.
We note that each element of icoord and dcoord (I will call this seg) has four points:
seg[1] seg[2]
+-------------+
| |
+ seg[0] + seg[3]
The vertical bars are fine as straight lines between the two points, but we need more points between seg[1] and seg[2] (the horizontal bar, which will need to become an arc).
This function will add more points in those positions and can be called on both xs and ys in the plotting function:
def smoothsegment(seg, Nsmooth=100):
return np.concatenate([[seg[0]], np.linspace(seg[1], seg[2], Nsmooth), [seg[3]]])
Now we must modify the plotting function to calculate the radial coordinates. Some experimentation has led to the log formula I am using, based on the other answer which also uses log scale. I've left a gap open on the right for the radial labels and done a very rudimentary mapping of the "icoord" coordinates to the radial ones so that the labels correspond to the ones in the rectangular plot. I don't know exactly how to handle the radial dimension. The numbers are correct for the log, but we probably want to map them as well.
def plot_dendrogram(icoord,dcoord,figsize, polar=False):
if polar:
dcoord = -np.log(dcoord+1)
# avoid a wedge over the radial labels
gap = 0.1
imax = icoord.max()
imin = icoord.min()
icoord = ((icoord - imin)/(imax - imin)*(1-gap) + gap/2)*2*numpy.pi
with plt.style.context("seaborn-white"):
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111, polar=polar)
for xs, ys in zip(icoord, dcoord):
if polar:
xs = smoothsegment(xs)
ys = smoothsegment(ys)
ax.plot(xs,ys, color="black")
ax.set_title(f"Polar= {polar}", fontsize=15)
if polar:
ax.spines['polar'].set_visible(False)
ax.set_rlabel_position(0)
Nxticks = 10
xticks = np.linspace(gap/2, 1-gap/2, Nxticks)
ax.set_xticks(xticks*np.pi*2)
ax.set_xticklabels(np.round(np.linspace(imin, imax, Nxticks)).astype(int))
Which results in the following figure:
First, I think you might benefit from this question.
Then, let's break down the objective: it is not very clear to me what you want to do, but I assume you want to get something that looks like this
(source, page 14)
To render something like this, you need to be able to render horizontal lines that appear as hemi-circles in polar coordinates. Then, it's a matter of mapping your horizontal lines to polar plot.
First, note that your radius are not normalized in this line:
if polar:
icoord, dcoord = cartesian_to_polar(icoord, dcoord)
you might normalize them by simply remapping icoord to [0;2pi).
Now, let's try plotting something simpler, instead of your complex plot:
icoord, dcoord = np.meshgrid(np.r_[1:10], np.r_[1:4])
# Plot the cartesian version
plot_dendrogram(icoord, dcoord, figsize=(8, 3), polar=False)
# Plot the polar version
plot_dendrogram(icoord, dcoord, figsize=(5, 5), polar=True)
Result is the following:
as you can see, the polar code does not map horizontal lines to semi-circles, therefore that is not going to work. Let's try with plt.polar instead:
plt.polar(icoord.T, dcoord.T)
produces
which is more like what we need. We need to fix the angles first, and then we shall consider that Y coordinate goes inward (while you probably want it going from center to border). It boils down to this
nic = (icoord.T - icoord.min()) / (icoord.max() - icoord.min())
plt.polar(2 * np.pi * nic, -dcoord.T)
which produces the following
Which is similar to what you need. Note that straight lines remain straight, and are not replaced with arcs, so you might want to resample them in your for loop.
Also, you might benefit from single color and log-scale to make reading easier
plt.subplots(figsize=(10, 10))
ico = (icoord.T - icoord.min()) / (icoord.max() - icoord.min())
plt.polar(2 * np.pi * ico, -np.log(dcoord.T), 'b')
I process geographical information and present the results using
matplotlib. All input is lattitude/longitude [degree]. I convert into
x/y [meter] for my calculations. And I present my results in
lattitude/longitude. The problem is to get the graphs aspect-ratio
right: All graphs are too wide. Is there a standard procedure to set the
correct aspect-ratio so I can simply draw my scatter and other diagrams
using lat/lon and the result has the correct shape? On screen and on
paper (png)?
[added this part later]
This is a bare-bone stripped version of my problem. I need actual lat/lon values
around the axes and an accurate shape (square). Right now it appears wide (2x).
import math
import matplotlib.pyplot as plt
import numpy as np
from pylab import *
w=1/math.cos(math.radians(60.0))
plt_area=[0,w,59.5,60.5] #60deg North, adjacent to the prime meridian
a=np.zeros(shape=(300,300))
matshow(a, extent=plt_area)
plt.grid(False)
plt.axis(plt_area)
fig = plt.gcf()
fig.set_size_inches(8,8)
fig.subplots_adjust(left=0.1, right=0.9, bottom=0.1, top=0.9)
plt.show()
It seems I found the solution.
And I found it here: How can I set the aspect ratio in matplotlib?
import math
import matplotlib.pyplot as plt
import numpy as np
w=1/math.cos(math.radians(60.0))
plt_area=[0,w,59.5,60.5] #square area
a=np.zeros(shape=(300,300))
fig = plt.figure()
ax = fig.add_subplot(111)
ax.imshow(a)
plt.grid(False)
ax.axis(plt_area)
fig = plt.gcf()
fig.set_size_inches(8,8)
ax.set_aspect(w)
fig.subplots_adjust(left=0.1, right=0.9, bottom=0.1, top=0.9)
plt.show()
In matplotlib I usually change the figure size like this:
import matplotlib.pyplot as plt
plt.clf()
fig = plt.figure()
fig_p = plt.gcf()
fig_p.set_size_inches(8, 8) # x, y
However this sets the dimensions for the figure outer dimensions, not the plot area. You can change the plot area relative to the figure size given in ratios of the total figure size lengths of x and y respectively:
fig.subplots_adjust(left=0.1, right=0.9, bottom=0.1, top=0.9)
As long as the the relative ratios stay symmetrically the aspect ratio should be the same for the plot are.
Example 1:
plt.clf()
fig = plt.figure()
fig_p = plt.gcf()
fig_p.set_size_inches(5, 5) # x, y for figure canvas
# Relative distance ratio between origin of the figure and max extend of canvas
fig.subplots_adjust(left=0.2, right=0.8, bottom=0.2, top=0.8)
ax1 = fig.add_subplot(111)
xdata = [rand()*10 for i in xrange(100)]
ydata = [rand()*1 for i in xrange(100)]
ax1.plot(xdata, ydata, '.b', )
ax1.set_xlabel('Very Large X-Label', size=30)
plt.savefig('squareplot.png', dpi=96)
Example 2:
fig.subplots_adjust(left=0.0, right=1.0, bottom=0.0, top=1.0)
Plot area fills the figure size completely:
Don't try to fix this by fiddling fig.set_size_inches() or fig.subplots_adjust() or by changing your data; instead use a Mercator projection.
You can get a quick and dirty Mercator projection by using an aspect ratio of the reciprocal of the cosine of the mean latitude of your data. This is "pretty good" for data contained in about 1 degree of latitude, which is about 100 km. (You have to decide if, for your application, this is "good enough". If it isn't, you really have to consider some serious geographical projection libraries...)
Example:
from math import cos, radians
import matplotlib.pyplot as plt
import numpy as np
# Helsinki 60.1708 N, 24.9375 E
# Helsinki (lng, lat)
hels = [24.9375, 60.1708]
# a point 100 km directly north of Helsinki
pt_N = [24.9375, 61.0701]
# a point 100 km east of Helsinki along its parallel
pt_E = [26.7455, 60.1708]
coords = np.array([pt_N, hels, pt_E])
plt.figure()
plt.plot(coords[:,0], coords[:,1])
# either of these will estimate the "central latitude" of your data
# 1) do the plot, then average the limits of the y-axis
central_latitude = sum(plt.axes().get_ylim())/2.
# 2) actually average the latitudes in your data
central_latitude = np.average(coords, 0)[1]
# calculate the aspect ratio that will approximate a
# Mercator projection at this central latitude
mercator_aspect_ratio = 1/cos(radians(central_latitude))
# set the aspect ratio of the axes to that
plt.axes().set_aspect(mercator_aspect_ratio)
plt.show()
I picked Helsinki for the example since at that latitude the aspect ratio is almost 2... because two degrees of longitude is the about same distance as one degree of latitude.
To really see this work: a) run the above, b) resize the window. Then comment out the call to set_aspect() and do the same. In the first case, the correct aspect ratio is maintained, in the latter you get nonsensical stretching.
The points 100km north and east of Helsinki were calculated/confirmed by the EXCELLENT page calculating distances between lat/lng points at Movable Type Scripts