Related
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')
I can do this with matplotlib I want to plot a torus and vary the coloring of the surface based on the small angle parameter. More generally, how do you color individual patches when you plot a surface with Mayavi?
I can do this easily in matplotlib using the 'facecolors' parameter when I plot, but because I need better 3D rendering I have to use Mayavi.
Moreover, here is how you can color individual points in Mayavi:
Plotting 3D points with different colors in Mayavi (Python)
But I haven't found something similar to work for surfaces.
From the documentation, the coloring is specified by default by the elevation z but I am not sure how I can change this to be specified based on some array I can pass to the plotting function.
Is there a way, to change this dependency?
'''
from mayavi import mlab
import numpy as np
mlab.options.offscreen = True
R = 2
r = 1
theta = np.linspace(0, 2 * np.pi, 20)
phi = np.linspace(0, 2 * np.pi, 20)
torus = np.zeros((3,20,20))
for i in range(0,20):
for j in range(0,20):
torus[0][i][j] = (R + r * np.cos(phi[j])) * np.cos(theta[i])
torus[1][i][j] = (R + r * np.cos(phi[j])) * np.sin(theta[i])
torus[2][i][j] = r * np.sin(phi[j])
mlab.mesh(torus[0], torus[1], torus[2])
mlab.show()
'''
I want the result to look like the following:
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 know that matplotlib and scipy can do bicubic interpolation:
http://matplotlib.org/examples/pylab_examples/image_interp.html
http://docs.scipy.org/doc/scipy/reference/tutorial/interpolate.html
http://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp2d.html
I also know that it is possible to draw a map of the world with matplotlib:
http://matplotlib.org/basemap/users/geography.html
http://matplotlib.org/basemap/users/examples.html
http://matplotlib.org/basemap/api/basemap_api.html
But can I do a bicubic interpolation based on 4 data points and only color the land mass?
For example using these for 4 data points (longitude and latitude) and colors:
Lagos: 6.453056, 3.395833; red HSV 0 100 100 (or z = 0)
Cairo: 30.05, 31.233333; green HSV 90 100 100 (or z = 90)
Johannesburg: -26.204444, 28.045556; cyan HSV 180 100 100 (or z = 180)
Mogadishu: 2.033333, 45.35; purple HSV 270 100 100 (or z = 270)
I am thinking that it must be possible to do the bicubic interpolation across the range of latitudes and longitudes and then add oceans, lakes and rivers on top of that layer? I can do this with drawmapboundary. Actually there is an option maskoceans for this:
http://matplotlib.org/basemap/api/basemap_api.html#mpl_toolkits.basemap.maskoceans
I can interpolate the data like this:
xnew, ynew = np.mgrid[-1:1:70j, -1:1:70j]
tck = interpolate.bisplrep(x, y, z, s=0)
znew = interpolate.bisplev(xnew[:,0], ynew[0,:], tck)
Or with scipy.interpolate.interp2d:
http://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp2d.html
Here it is explained how to convert to map projection coordinates:
http://matplotlib.org/basemap/users/mapcoords.html
But I need to figure out how to do this for a calculated surface instead of individual points. Actually there is an example of such a topographic map using external data, which I should be able to replicate:
http://matplotlib.org/basemap/users/examples.html
P.S. I am not looking for a complete solution. I would much prefer to solve this myself. Rather I am looking for suggestions and hints. I have been using gnuplot for more than 10 years and only switched to matplotlib within the past few weeks, so please don't assume I know even the simplest things about matplotlib.
I think this is what you are looking for (roughly). Note the crucial things are masking the data array before you plot the pcolor and passing in the hsv colormap (Docs: cmap parameter for pcolormesh and available colormaps).
I've kept the code for plotting the maps quite close to the examples so it should be easy to follow. I've kept your interpolation code for the same reason. Note that the interpolation is linear rather than cubic - kx=ky=1 - because you don't give enough points to do cubic interpolation (you'd need at least 16 - scipy will complain with less saying that "m must be >= (kx+1)(ky+1)", although the constraint is not mentioned in the documentation).
I've also extended the range of your meshgrid and kept in lat / lon for x and y throughout.
Code
from mpl_toolkits.basemap import Basemap,maskoceans
import matplotlib.pyplot as plt
import numpy as np
from scipy import interpolate
# set up orthographic map projection with
# perspective of satellite looking down at 0N, 20W (Africa in main focus)
# use low resolution coastlines.
map = Basemap(projection='ortho',lat_0=0,lon_0=20,resolution='l')
# draw coastlines, country boundaries
map.drawcoastlines(linewidth=0.25)
map.drawcountries(linewidth=0.25)
# Optionally (commented line below) give the map a fill colour - e.g. a blue sea
#map.drawmapboundary(fill_color='aqua')
# draw lat/lon grid lines every 30 degrees.
map.drawmeridians(np.arange(0,360,30))
map.drawparallels(np.arange(-90,90,30))
data = {'Lagos': (6.453056, 3.395833,0),
'Cairo': (30.05, 31.233333,90),
'Johannesburg': (-26.204444, 28.045556,180),
'Mogadishu': (2.033333, 45.35, 270)}
x,y,z = zip(*data.values())
xnew, ynew = np.mgrid[-30:60:0.1, -50:50:0.1]
tck = interpolate.bisplrep(x, y, z, s=0,kx=1,ky=1)
znew = interpolate.bisplev(xnew[:,0], ynew[0,:], tck)
znew = maskoceans(xnew, ynew, znew)
col_plot = map.pcolormesh(xnew, ynew, znew, latlon=True, cmap='hsv')
plt.show()
Output
Observe that doing the opposite, that is putting a raster on the sea and lay a mask over the continents, is easy as pie. Simply use map.fillcontinents(). So the basic idea of this solution is to modify the fillcontinents function so that it lays polygons over the oceans.
The steps are:
Create a large circle-like polygon that covers the entire globe.
Create a polygon for each shape in the map.coastpolygons array.
Cut the shape of the landmass polygon away from the circle using shapely and its difference method.
Add the remaining polygons, which have the shape of the oceans, on the top, with a high zorder.
The code:
from mpl_toolkits.basemap import Basemap
import numpy as np
from scipy import interpolate
from shapely.geometry import Polygon
from descartes.patch import PolygonPatch
def my_circle_polygon( (x0, y0), r, resolution = 50 ):
circle = []
for theta in np.linspace(0,2*np.pi, resolution):
x = r * np.cos(theta) + x0
y = r * np.sin(theta) + y0
circle.append( (x,y) )
return Polygon( circle[:-1] )
def filloceans(the_map, color='0.8', ax=None):
# get current axes instance (if none specified).
if not ax:
ax = the_map._check_ax()
# creates a circle that covers the world
r = 0.5*(map.xmax - map.xmin) # + 50000 # adds a little bit of margin
x0 = 0.5*(map.xmax + map.xmin)
y0 = 0.5*(map.ymax + map.ymin)
oceans = my_circle_polygon( (x0, y0) , r, resolution = 100 )
# for each coastline polygon, gouge it out of the circle
for x,y in the_map.coastpolygons:
xa = np.array(x,np.float32)
ya = np.array(y,np.float32)
xy = np.array(zip(xa.tolist(),ya.tolist()))
continent = Polygon(xy)
## catches error when difference with lakes
try:
oceans = oceans.difference(continent)
except:
patch = PolygonPatch(continent, color="white", zorder =150)
ax.add_patch( patch )
for ocean in oceans:
sea_patch = PolygonPatch(ocean, color="blue", zorder =100)
ax.add_patch( sea_patch )
########### DATA
x = [3.395833, 31.233333, 28.045556, 45.35 ]
y = [6.453056, 30.05, -26.204444, 2.033333]
z = [0, 90, 180, 270]
# set up orthographic map projection
map = Basemap(projection='ortho', lat_0=0, lon_0=20, resolution='l')
## Plot the cities on the map
map.plot(x,y,".", latlon=1)
# create a interpolated mesh and set it on the map
interpol_func = interpolate.interp2d(x, y, z, kind='linear')
newx = np.linspace( min(x), max(x) )
newy = np.linspace( min(y), max(y) )
X,Y = np.meshgrid(newx, newy)
Z = interpol_func(newx, newy)
map.pcolormesh( X, Y, Z, latlon=1, zorder=3)
filloceans(map, color="blue")
Voilà:
I'm making wind vector barb plots using the matplotlib barb function and basemap in python.
I have a list of vectors (wind observations) at arbitrary latitudes and longitudes, i.e. not on a regular grid.
I need to rotate the vectors onto the map projection before plotting or the barbs point in the wrong direction. What is the best way to do this?
e.g.
import numpy
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
# Define locations of my vectors
lat = numpy.array([50.1,46.2,51.6,52.2,54.4])
lon = numpy.array([-3.3,-1.0,-5.2,-1.2,0.2])
# Define some east-west vectors to illustrate the problem
u = numpy.array([5,5,5,5,5])
v = numpy.array([0,0,0,0,0])
# Set up map projection
m = Basemap(llcrnrlon=-15.,llcrnrlat=46.,urcrnrlon=15.,urcrnrlat=59.,
projection='lcc',lat_1=40.,lat_2=50.,lon_0=-50.,
resolution ='l')
# Calculate positions of vectors on map projection
x,y = m(lon,lat)
# Draw barbs
m.barbs(x,y,u,v, length=7, color='red')
# Draw some grid lines for reference
parallels = numpy.arange(-80.,90,20.)
meridians = numpy.arange(0.,360.,20.)
m.drawparallels(parallels)
m.drawmeridians(meridians)
m.drawcoastlines(linewidth=0.5)
plt.show()
Note that in the plot, the vectors do not point east-west.
I have tried using the rotate_vector and transform_vector routines, but these only work for gridded vector data.
Is there a routine to rotate the vectors onto the map projection for an arbitrary list of lat,lon u,v pairs?
Any help would be much appreciated!
For people with gridded data who stumpled upon this question
Rather use the built-in function rotate_vector, you can find it here:
http://matplotlib.org/basemap/api/basemap_api.html
Your problem is that you're specifying your u and v in lat, long. At the same time, you're specifying your x and y in map coordinates. barbs seems to expect both of them in map coordinates, rather than a mix.
The simplest way is to just calculate the endpoints to get the components. (My description makes no sense, so here's what I had in mind:)
x, y = m(lon, lat)
x1, y1 = m(lon+u, lat+v)
u_map, v_map = x1-x, y1-y
You'll then need to rescale the magnitudes, as well. As a full example:
import numpy
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
# Define locations of my vectors
lat = numpy.array([50.1,46.2,51.6,52.2,54.4])
lon = numpy.array([-3.3,-1.0,-5.2,-1.2,0.2])
# Define some east-west vectors to illustrate the problem
u = numpy.array([5,5,5,5,5])
v = numpy.array([0,0,0,0,0])
# Set up map projection
m = Basemap(llcrnrlon=-15.,llcrnrlat=46.,urcrnrlon=15.,urcrnrlat=59.,
projection='lcc',lat_1=40.,lat_2=50.,lon_0=-50.,
resolution ='l')
# Calculate positions of vectors on map projection
x,y = m(lon,lat)
# Calculate the orientation of the vectors
x1, y1 = m(lon+u, lat+v)
u_map, v_map = x1-x, y1-y
# Rescale the magnitudes of the vectors...
mag_scale = np.hypot(u_map, v_map) / np.hypot(u, v)
u_map /= mag_scale
v_map /= mag_scale
# Draw barbs
m.barbs(x,y,u_map,v_map, length=7, color='red')
# Draw some grid lines for reference
parallels = numpy.arange(-80.,90,20.)
meridians = numpy.arange(0.,360.,20.)
m.drawparallels(parallels)
m.drawmeridians(meridians)
m.drawcoastlines(linewidth=0.5)
plt.show()