Here's one example of Basemap:
fig = plt.figure(figsize=(10,6))
ax = fig.add_subplot(121)
ax.set_title('Default')
# miller projection
map = Basemap(projection='mill',lon_0=180)
# plot coastlines, draw label meridians and parallels.
map.drawcoastlines()
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
ax = fig.add_subplot(122)
ax.set_title('Add offset')
# miller projection
map = Basemap(projection='mill',lon_0=180)
# plot coastlines, draw label meridians and parallels.
map.drawcoastlines()
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],xoffset=100,yoffset=100)
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],xoffset=100,yoffset=100)
I want to add more space between the xlabel/ylabel and axis.
But, the space is smaller when xoffset and yoffset are added.
basemap is not being actively developed anymore, but maintenance still continues for some time. This means that things that break because of changes in other packages will still be fixed, but no new features will be added. Anyway, the fixing part may take some time and I'm guessing that the xoffset feature for the parallels and meridians is suffering from that. However, looking at the basemap documentation, the functionality of xoffset and yoffset are described as
xoffset: label offset from edge of map in x-direction (default is 0.01
times width of map in map projection coordinates).
yoffset: label
offset from edge of map in y-direction (default is 0.01 times height
of map in map projection coordinates).
This is easy enough to emulate by manipulating the Text objects that drawparallels() and drawmeridians() produce. The results of these functions are stored in a dict that contains a tuple of lists for each plotted parallel/meridian, the second of which contains the text labels. A Text object has a get_position() and a set_position() method, which, in combination with the axes limits and the definition above, can be used to compute the offset:
from matplotlib import pyplot as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
fig = plt.figure(figsize=(10,6))
ax = fig.add_subplot(121)
ax.set_title('Default')
# miller projection
map = Basemap(projection='mill',lon_0=180)
# plot coastlines, draw label meridians and parallels.
map.drawcoastlines()
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
ax = fig.add_subplot(122)
ax.set_title('Add offset')
# miller projection
map = Basemap(projection='mill',lon_0=180)
# plot coastlines, draw label meridians and parallels.
map.drawcoastlines()
##getting axes dimensions
x0,x1 = ax.get_xlim()
w = x1-x0
y0,y1 = ax.get_ylim()
h = y1-y0
xoffset = 0.05
yoffset = 0.05
result = map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
##
for key, (lines,texts) in result.items():
for text in texts:
x,y = text.get_position()
text.set_position((x0-xoffset*w,y))
result = map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
for key, (lines,texts) in result.items():
for text in texts:
x,y = text.get_position()
text.set_position((x,y0-yoffset*h))
plt.show()
The resulting plot looks like this:
I think you are not using the correct unit. #Thomas Kühn quoted the basemap doc:
xoffset: label offset from edge of map in x-direction (default is 0.01
times width of map in map projection coordinates).
yoffset: label offset from edge of map in y-direction (default is 0.01
times height of map in map projection coordinates).
Note that it is defaulted to 1% of the domain span measured in map projection coordinates.
If you check the span in y-axis of the mill projection you used, the length has 8 digits, so no wonder yoffset=100 gives no visual offset.
So an easier way is to modify the offset using the actual domain span, like:
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=0.01*abs(map.ymax-map.ymin))
This gives the same offset as default, i.e. 1% of the domain span (See figure (b) below). Changing 0.01 to 0.03 will be 3x of that (figure (c)).
If you instead use cyl projection which uses degree of latitude/longitude as units, the offsets are also measured in degrees, then yoffset=100 will be an insane offset. Figure (f) uses a yoffset=30, note that is the same distance as the distance from 60S to 90S.
The script to generate the figure:
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
fig = plt.figure(figsize=(12,6))
def drawMap(proj):
map = Basemap(projection=proj,lon_0=180)
map.drawcoastlines()
return map
# miller projection
ax = fig.add_subplot(231)
ax.set_title('(a) Mill, Default')
map=drawMap('mill')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
ax = fig.add_subplot(232)
ax.set_title('(b) Mill, add same offset as default')
map=drawMap('mill')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
xoffset=0.01*abs(map.xmax-map.xmin))
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=0.01*abs(map.ymax-map.ymin))
ax = fig.add_subplot(233)
ax.set_title('(c) Mill, add 3x offset as default')
map=drawMap('mill')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
xoffset=0.03*abs(map.xmax-map.xmin))
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=0.03*abs(map.ymax-map.ymin))
ax = fig.add_subplot(234)
ax.set_title('(d) Cyl, Default')
map=drawMap('cyl')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
ax = fig.add_subplot(235)
ax.set_title('(e) Cyl, add same offset as default')
map=drawMap('cyl')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
xoffset=0.01*abs(map.xmax-map.xmin))
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=0.01*abs(map.ymax-map.ymin))
ax = fig.add_subplot(236)
ax.set_title('(f) Cyl, add 30 degree offset')
map=drawMap('cyl')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
xoffset=0.03*abs(map.xmax-map.xmin))
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=30)
fig.subplots_adjust(hspace=0.01)
fig.show()
Related
I want to plot a vector field with vectors representing a displacement between one point to another on the map with cartopy.
My code works as expected when using the PlateCarree() transformation, but arrow length is several orders of magnitude off for all the other projections I tested.
Here is a MWE that should illustrate quite clearly the issue:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy
# Want to test for several different projections
projections = [
ccrs.PlateCarree(),
ccrs.EqualEarth(),
ccrs.Mollweide(),
ccrs.AzimuthalEquidistant(),
]
# ALl the coordinates will be given in the PlateCarree coordinate system.
coordinate_ccrs = ccrs.PlateCarree()
# We want N**2 points over the latitude/longitude values.
N = 5
lat, lon = numpy.meshgrid(numpy.linspace(-80, 80, N), numpy.linspace(-170, 170, N))
lat, lon = lat.flatten(), lon.flatten()
# We want arrows to appear, let make a small perturbation and try
# to do an arrow from (lon, lat) to (lon + perturbation, lat + perturbation).
rng = numpy.random.default_rng()
perturbation_amplitude = 10
lat_perturbation = perturbation_amplitude * rng.random(N * N)
lon_perturbation = perturbation_amplitude * rng.random(N * N)
# Create the matplotlib figure and axes, no projection for the moment as this
# will be changed later.
fig, axes = plt.subplots(2, 2)
axes = axes.flatten()
for i, projection in enumerate(projections):
# Replace the existing ax with an ax with the desired projection.
ax = axes[i]
fig.delaxes(ax)
ax = axes[i] = fig.add_subplot(2, 2, i + 1, projection=projection)
# Make the plot readable.
ax.set_global()
ax.gridlines(draw_labels="x")
# Non pertubed points are plotted in black.
ax.plot(lon, lat, "k.", ms=5, transform=coordinate_ccrs)
# Perturbed points are plotted in red.
ax.plot(
lon + lon_perturbation,
lat + lat_perturbation,
"r.",
ms=5,
transform=coordinate_ccrs,
)
# We try to draw arrows from a given black dot to its corresponding
# red dot.
ax.quiver(
lon,
lat,
lon_perturbation,
lat_perturbation,
transform=coordinate_ccrs,
# From https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.quiver.html?highlight=quiver#matplotlib.axes.Axes.quiver
# look at the documentation of the "scale_unit" parameter.
# The next 3 parameters are what matplotlib tell us to do. From
# matplotlib documentation:
# To plot vectors in the x-y plane, with u and v having the same units
# as x and y, use angles='xy', scale_units='xy', scale=1.
angles="xy",
scale_units="xy",
scale=1,
# Simply make the arrows nicer, removing these last 3 parameters do not
# solve the issue.
minshaft=2,
minlength=0.5,
width=0.002,
)
# Show everything
plt.show()
which display on the screen the following image:
The PlateCarree transformation is the only one displaying arrows. In fact, there are arrows in the other 3 projections, but I order to see them I need to scale them by 10000 with scale=0.00001 in the call to quiver, which gives:
Did I make a mistake when using cartopy API, is this expected behaviour and I missed something in the documentation, or is this a bug?
while there's quite some debate on github about cartopy's implementation of quiver-plot transformations GitHub-issues there is in fact a way on how to get your plot look as you want it to look...
However, while thinking about this... I noticed that there's a thing that you might want to consider when using projected quiver-plots...
As I see it, the re-projected arrows would would most probably need to be curved to really visualize the same direction as provided in the original data!
(in the input-crs the arrow points as a straight line from point A to B, but if you re-project the points, the "straight line" that connected A and B is now in general a curved line, and so if the original direction was correct, I think the new direction should be indicated as a curved arrow...)
That being said, you could achieve what you want by transforming the points manually instead of letting cartopy do the job:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy
# Want to test for several different projections
projections = [
ccrs.PlateCarree(),
ccrs.EqualEarth(),
ccrs.Mollweide(),
ccrs.AzimuthalEquidistant(),
]
# ALl the coordinates will be given in the PlateCarree coordinate system.
coordinate_ccrs = ccrs.PlateCarree()
# We want N**2 points over the latitude/longitude values.
N = 5
lat, lon = numpy.meshgrid(numpy.linspace(-80, 80, N), numpy.linspace(-170, 170, N))
lat, lon = lat.flatten(), lon.flatten()
# We want arrows to appear, let make a small perturbation and try
# to do an arrow from (lon, lat) to (lon + perturbation, lat + perturbation).
rng = numpy.random.default_rng()
perturbation_amplitude = 10
lat_perturbation = perturbation_amplitude * rng.random(N * N)
lon_perturbation = perturbation_amplitude * rng.random(N * N)
# Create the matplotlib figure and axes, no projection for the moment as this
# will be changed later.
fig, axes = plt.subplots(2, 2)
axes = axes.flatten()
for i, projection in enumerate(projections):
# Replace the existing ax with an ax with the desired projection.
ax = axes[i]
fig.delaxes(ax)
ax = axes[i] = fig.add_subplot(2, 2, i + 1, projection=projection)
# Make the plot readable.
ax.set_global()
ax.gridlines(draw_labels="x")
# Non pertubed points are plotted in black.
ax.plot(lon, lat, "k.", ms=5, transform=coordinate_ccrs)
# Perturbed points are plotted in red.
ax.plot(
lon + lon_perturbation,
lat + lat_perturbation,
"r.",
ms=5,
transform=coordinate_ccrs,
)
xy_start = projection.transform_points(coordinate_ccrs, lon, lat)[:,:-1].T
xy_end = projection.transform_points(coordinate_ccrs, lon + lon_perturbation,
lat + lat_perturbation)[:,:-1].T
# We try to draw arrows from a given black dot to its corresponding
# red dot.
ax.quiver(
*xy_start,
*(xy_end - xy_start),
# From https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.quiver.html?highlight=quiver#matplotlib.axes.Axes.quiver
# look at the documentation of the "scale_unit" parameter.
# The next 3 parameters are what matplotlib tell us to do. From
# matplotlib documentation:
# To plot vectors in the x-y plane, with u and v having the same units
# as x and y, use angles='xy', scale_units='xy', scale=1.
angles="xy",
scale_units="xy",
scale=1,
# Simply make the arrows nicer, removing these last 3 parameters do not
# solve the issue.
minshaft=2,
minlength=0.5,
width=0.002,
)
# Show everything
plt.show()
I need to put a transparent boxplot on another image with grid. I create an image with a size based on data I pass. Once the boxplot is drawn, the initial size of the image has changed. Here my part of code with further explications:
png_width = 280
png_height = 120
px = 1 / plt.rcParams['figure.dpi'] # pixel in inches
fig_width = math.ceil((png_width / 60) * (max(data) - min(data)))
fig_cnt += 1
fig = plt.figure(fig_cnt, figsize=(fig_width * px, png_height * px), frameon=False, clear=True)
ax = fig.add_subplot(111)
bp = ax.boxplot(data, vert=False, widths=0.2, whis=[0, 100], )
for median in bp['medians']:
median.set(color='#000000')
plt.axis('off')
plt.savefig(filepath, bbox_inches='tight', pad_inches=0, transparent=True)
plt.close()
For a given set of data I have min=3 and max=60. fig_width=266px which is correct. Now I want to draw a boxplot where the distance from the first whisker to the second is also 266px. In my example the saved image has only 206px. I do not need any axis on the boxplot, only the boxplot itself. Why isn't the initial image size not maintained?
There is a default margin around the subplots, so set the axes to the whole fig area by
fig.subplots_adjust(0, 0, 1, 1)
and your saved image is exactly 266 px wide.
The distance between the whiskers' ends will be smaller due to autoscaling. If you want the whiskers to use up the entire x-axis you can set the x limits accordingly:
ax.set_xlim(bp['whiskers'][0].get_xdata()[1], bp['whiskers'][1].get_xdata()[1])
(you may want to increase the limits a bit to see the full line width of the caps)
I am trying to plot some data in polar coordinates (I am currently using the polar projection):
The code I am using is the following:
import matplotlib.pyplot as plt
import numpy as np
# Create radial and angular array
r = np.linspace(1.0,10,11)
t = np.linspace(0.0,0.5*np.pi,101)
# Define the quantity that I want to plot
z = np.zeros((len(t),len(r)))
for yval in range(len(r)):
z[:,yval] = np.cos(16.0*t)/r[yval]
#Create the figure
f = plt.figure(figsize=(13,8))
ax = plt.subplot(111, projection='polar')
ax.set_rorigin(-1)
#Plot the data
pcm = ax.pcolormesh(t,r,z.T,cmap = 'hot',shading='gouraud')
ax.set_xlim([0.0,0.5*np.pi])
ax.set_ylim([1.0,10.0])
#Add colorbar and show
bar = f.colorbar(pcm)
plt.show()
So far I have no problem, but I would like to zoom on a particular region of this plot.
However, when I set the axes range the axes is still polar, therefore I cannot zoom on a "cartesian" region of the domain (i.e. a square box).
A possible option would be to transform the data into cartesian coordinates, but when I do it I lose a lot of resolution in the inner part of the domain, which is something that I should absolutely avoid.
How can I select a rectangular zone of a plot in polar coordinates without transforming by hand the data? And in case I have to switch to cartesian coordinates, is there any matplotlib or python function that does it while taking care of the resolution in the inner regions of the domain?
Thanks in advance
You can create an X, Y mesh yourself that is has a higher resolution on the inner part of the domain and use that with ax.pcolormesh()
# Create radial and angular array
r = np.linspace(1.0,10,11)
t = np.linspace(0.0,0.5*np.pi,101)
# Define the quantity that I want to plot
z = np.zeros((len(t),len(r)))
for yval in range(len(r)):
z[:,yval] = np.cos(16.0*t)/r[yval]
#Create the figure, bigger figsize to make the resulting plot square
f = plt.figure(figsize=(13,10))
ax = plt.subplot(111) # Drop back to XY coordinates
# Generate the XY corners of the colormesh
X = np.array([[ri*np.cos(j) for j in t] for ri in r])
Y = np.array([[ri*np.sin(j) for j in t] for ri in r])
#Plot the data
pcm = ax.pcolormesh(X,Y,z.T,cmap = 'hot',shading='gouraud')
#Add colorbar and show
bar = f.colorbar(pcm)
plt.show()
The figure from the question
The figure generated by code above
A way to do this is to create an expanded polar plot and then clip a rectangle of it. A picture is worth a thousand words:
Here is a function that allows you to do so. The arguments are the original axes, the xlims and ylims of the region to be zoomed and the inset axes bounds (x0, y0, width, height) in the original axes coordinates. The function outputs a cartesian ax with the specified limits, a polar axes where you can plot and the rmax value you need to set AFTER plotting (if you do it before, it will change after plotting).
def create_polar_zoom_inset(ax, xlims, ylims, inset_bounds):
# Create cartesian axes for inset
ax_inset_cart = ax.inset_axes(inset_bounds)
ax_inset_cart.set_xlim(xlims)
ax_inset_cart.set_ylim(ylims)
# Calculate location of expanded polar inset
# Scale factor from data to axes coordinates
xscalefactor = inset_bounds[2]/(xlims[1] - xlims[0])
yscalefactor = inset_bounds[3]/(ylims[1] - ylims[0])
# Center of expanded polar inset
center_inset_polar = [
inset_bounds[0] - xlims[0]*xscalefactor,
inset_bounds[1] - ylims[0]*yscalefactor
]
# Max value of r in the inset
rmax_inset = 2*np.sqrt(np.power(xlims, 2).max() + np.power(ylims, 2).max())
# Size of the expanded polar inset
size_inset_polar = [2*rmax_inset*xscalefactor, 2*rmax_inset*yscalefactor]
# Create expanded polar inset
polar_inset_bounds = [
center_inset_polar[0] - 0.5*size_inset_polar[0],
center_inset_polar[1] - 0.5*size_inset_polar[1],
size_inset_polar[0],
size_inset_polar[1]
]
ax_inset_polar = ax.inset_axes(polar_inset_bounds, projection="polar")
ax_inset_polar.set_facecolor("None")
# Remove tick labels from expanded polar inset
ax_inset_polar.xaxis.set_ticklabels([])
ax_inset_polar.yaxis.set_ticklabels([])
# Clip elements of the expanded inset outside the cartesian inset
ax_inset_polar.patch = ax_inset_cart.patch
for axis in [ax_inset_polar.xaxis, ax_inset_polar.yaxis]:
axis.set_clip_path(ax_inset_cart.patch)
ax_inset_polar.spines['polar'].set_clip_path(ax_inset_cart.patch)
return ax_inset_cart, ax_inset_polar, rmax_inset
The code in your example is especially hard since the origin of the axes is not (0,0) but (-1,-1). That would need additional tinkering. But if we set rorigin to 0 (as it will be usually the case), the code would look as follows
# Create radial and angular array
r = np.linspace(1.0,10,11)
t = np.linspace(0.0,0.5*np.pi,101)
# Define the quantity that I want to plot
z = np.zeros((len(t),len(r)))
for yval in range(len(r)):
z[:,yval] = np.cos(16.0*t)/r[yval]
#Create the figure
f = plt.figure(figsize=(13,8))
ax = plt.subplot(111, projection='polar')
ax.set_rorigin(0)
#Plot the data
pcm = ax.pcolormesh(t,r,z.T,cmap = 'hot',shading='gouraud')
ax.set_xlim([0.0,0.5*np.pi])
ax.set_ylim([1.0,10.0])
#Add colorbar and show
bar = f.colorbar(pcm)
#Create inset
ax_c, ax_p, rmax_inset = create_polar_zoom_inset(
ax, xlims=[0., 2.], ylims=[1, 2], inset_bounds=[0.4, 0.3, 0.6, 0.3])
#Plot on inset
ax_p.pcolormesh(t,r,z.T,cmap = 'hot',shading='gouraud')
#Make rorigin and rmin coincide with the original plot
ax_p.set_rorigin(0)
ax_p.set_rmin(1)
#Set rmax
ax_p.set_rmax(rmax_inset)
plt.show()
I want to have the markers of a scatter plot match a radius given in the data coordinates.
I've read in pyplot scatter plot marker size, that the marker size is given as the area of the marker in points^2.
I tried transforming the given radius into points via axes.transData and then calculating the area via pi * r^2, but I did not succeed.
Maybe I did the transformation wrong.
It could also be that running matplotlib from WSL via VcXsrv is causing this problem.
Here is an example code of what I want to accomplish, with the results as an image below the code:
import matplotlib.pyplot as plt
from numpy import pi
n = 16
# create a n x n square with a marker at each point
x_data = []
y_data = []
for x in range(n):
for y in range(n):
x_data.append(x)
y_data.append(y)
fig,ax = plt.subplots(figsize=[7,7])
# important part:
# calculate the marker size so that the markers touch
# radius in data coordinates:
r = 0.5
# radius in display coordinates:
r_ = ax.transData.transform([r,0])[0] - ax.transData.transform([0,0])[0]
# marker size as the area of a circle
marker_size = pi * r_**2
ax.scatter(x_data, y_data, s=marker_size, edgecolors='black')
plt.show()
When I run it with s=r_ I get the result on the left and with s=marker_size I get the result on the right of the following image:
The code looks perfectly fine. You can see this if you plot just 4 points (n=2):
The radius is (almost) exactly the r=0.5 coordinate-units that you wanted to have. wait, almost?!
Yes, the problem is that you determine the coordinate-units-to-figure-points size before plotting, so before setting the limits, which influence the coordinate-units but not the overall figure size...
Sounded strange? Perhaps. The bottom line is that you determine the coordinate transformation with the default axis-limits ((0,1) x (0,1)) and enlarges them afterwards to (-0.75, 15.75)x(-0.75, 15.75)... but you are not reducing the marker-size.
So either set the limits to the known size before plotting:
ax.set_xlim((0,n-1))
ax.set_ylim((0,n-1))
The complete code is:
import matplotlib.pyplot as plt
from numpy import pi
n = 16
# create a n x n square with a marker at each point as dummy data
x_data = []
y_data = []
for x in range(n):
for y in range(n):
x_data.append(x)
y_data.append(y)
# open figure
fig,ax = plt.subplots(figsize=[7,7])
# set limits BEFORE plotting
ax.set_xlim((0,n-1))
ax.set_ylim((0,n-1))
# radius in data coordinates:
r = 0.5 # units
# radius in display coordinates:
r_ = ax.transData.transform([r,0])[0] - ax.transData.transform([0,0])[0] # points
# marker size as the area of a circle
marker_size = pi * r_**2
# plot
ax.scatter(x_data, y_data, s=marker_size, edgecolors='black')
plt.show()
... or scale the markers's size according to the new limits (you will need to know them or do the plotting again)
# plot with invisible color
ax.scatter(x_data, y_data, s=marker_size, color=(0,0,0,0))
# calculate scaling
scl = ax.get_xlim()[1] - ax.get_xlim()[0]
# plot correctly (with color)
ax.scatter(x_data, y_data, s=marker_size/scl**2, edgecolors='blue',color='red')
This is a rather tedious idea, because you need to plot the data twice but you keep the autosizing of the axes...
There obviously remains some spacing. This is due a misunderstanding of the area of the markers. We are not talking about the area of the symbol (in this case a circle) but of a bounding box of the marker (imagine, you want to control the size of a star or an asterix as marker... one would never calculate the actual area of the symbol).
So calculating the area is not pi * r_**2 but rather a square: (2*r_)**2
# open figure
fig,ax = plt.subplots(figsize=[7,7])
# setting the limits
ax.set_xlim((0,n-1))
ax.set_ylim((0,n-1))
# radius in data coordinates:
r = 0.5 # units
# radius in display coordinates:
r_ = ax.transData.transform([r,0])[0] - ax.transData.transform([0,0])[0] # points
# marker size as the area of a circle
marker_size = (2*r_)**2
# plot
ax.scatter(x_data, y_data, s=marker_size,linewidths=1)
#ax.plot(x_data, y_data, "o",markersize=2*r_)
plt.show()
As soon as you add an edge (so a non-zero border around the markers), they will overlap:
If even gets more confusing if you use plot (which is faster if all markers should have the same size as the docs state as "Notes"). The markersize is only the width (not the area) of the marker:
ax.plot(x_data, y_data, "o",markersize=2*r_,color='magenta')
I am trying to create a 3-D plot and a 2-D plot side-by-side in python. I need equal aspect ratios for both plots, which I managed using code provided by this answer: https://stackoverflow.com/a/31364297/125507. The problem I'm having now is how to effectively "crop" the 3-D plot so it doesn't take up so much white space. That is to say, I want to reduce the length of the X and Y axes while maintaining equal scale to the (longer) Z-axis. Here is a sample code and plot:
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np
def set_axes_equal(ax):
'''Make axes of 3D plot have equal scale so that spheres appear as spheres,
cubes as cubes, etc.. This is one possible solution to Matplotlib's
ax.set_aspect('equal') and ax.axis('equal') not working for 3D.
Input
ax: a matplotlib axis, e.g., as output from plt.gca().
'''
x_limits = ax.get_xlim3d()
y_limits = ax.get_ylim3d()
z_limits = ax.get_zlim3d()
x_range = abs(x_limits[1] - x_limits[0])
x_middle = np.mean(x_limits)
y_range = abs(y_limits[1] - y_limits[0])
y_middle = np.mean(y_limits)
z_range = abs(z_limits[1] - z_limits[0])
z_middle = np.mean(z_limits)
# The plot bounding box is a sphere in the sense of the infinity
# norm, hence I call half the max range the plot radius.
plot_radius = 0.5*max([x_range, y_range, z_range])
ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])
ax = [None]*2
fig = plt.figure()
ax[0] = fig.add_subplot(121, projection='3d', aspect='equal')
ax[1] = fig.add_subplot(122, aspect='equal')
nn = 30
phis = np.linspace(0,np.pi, nn).reshape(1,nn)
psis = np.linspace(0,np.pi*2,nn).reshape(nn,1)
ones = np.ones((nn,1))
el_h = np.linspace(-5, 5, nn).reshape(1,nn)
x_sph = np.sin(phis)*np.cos(psis)
y_sph = np.sin(phis)*np.sin(psis)
z_sph = np.cos(phis)*ones
x_elp = np.sin(phis)*np.cos(psis)*.25
y_elp = np.sin(phis)*np.sin(psis)*.25
z_elp = el_h*ones
ax[0].scatter(x_sph, y_sph, z_sph)
ax[0].scatter(x_elp, y_elp, z_elp)
ax[1].scatter(y_sph, z_sph)
ax[1].scatter(y_elp, z_elp)
for ii in range(2):
ax[ii].set_xlabel('X')
ax[ii].set_ylabel('Y')
ax[0].set_zlabel('Z')
set_axes_equal(ax[0])
plt.savefig('SphereElipse.png', dpi=300)
And here is its image output:
3-D and 2-D sphere and ellipse side-by-side
Clearly the 2D plot automatically modifies the length of the axes while maintaining the scale, but the 3D plot doesn't, leading to a tiny representation which does not well use the space allotted to its subplot. Is there any way to do this? This question is similar to an earlier unanswered question How do I crop an Axes3D plot with square aspect ratio?, except it adds the stipulation of multiple subplots, which means the answers provided there do not work.