I am producing a map of the world with pie charts in individual model grid boxes. I make the map and coastlines using cartopy. The pie charts I produce using inset_axes. Unfortunately the pie charts hide the coastlines and I'd like to see them clearly.
Minimum working example:
import cartopy.crs as ccrs
import numpy as np
import cartopy.feature as feature
import matplotlib.pyplot as plt
def plot_pie_inset(dataframe_pie,ilat_pie,ilon_pie,axis_main,width_local,alpha_local):
ax_sub= inset_axes(axis_main, width=width_local, height=width_local, loc=3, bbox_to_anchor=(ilat_pie, ilon_pie),bbox_transform=axis_main.figure.transFigure, borderpad=0.0)
wedges,texts= ax_sub.pie(dataframe_pie,colors=colors_dual)
for w in wedges:
w.set_linewidth(0.02)
w.set_alpha(alpha_local)
w.set_zorder(1)
plt.axis('equal')
colors_dual=['RosyBrown','LightBlue']
lat_list= np.arange(0.2,0.7,0.05)
fig= plt.figure()
ax_main= plt.subplot(1,1,1,projection=ccrs.PlateCarree())
ax_main.coastlines(zorder=3)
for ilat in np.arange(len(lat_list)):
plot_pie_inset([75,25],lat_list[ilat],0.72,ax_main,0.2,0.9)
plt.show()
I can see the coastlines by making the pie charts partially transparent by reducing the alpha value. However, this makes the colors somewhat muted. My aim is to have the coastlines as the topmost layer.
I have attempted to use 'zorder' to force the coastlines to the top layer. However, 'zorder' cannot be passed to inset_axes, nor to ax.pie so I've made the patches of color in pie charts translucent. This fails because the ax_main.coastlines does not have its own 'zorder'. The coastline zorder seems to be tied to that of ax_main. There is no benefit in increasing the zorder of ax_main.
Any suggestions greatly welcomed.
The problem is that each axes either lies on top or below another axes. So changing the zorder of artists within axes, does not help here. In principle, one could set the zorder of the axes themselves, putting the inset axes behind the main axes.
ax_sub.set_zorder(axis_main.get_zorder()-1)
Cartopy's GeoAxes uses its own background patch. This would then need to be set to invisble.
ax_main.background_patch.set_visible(False)
Complete example:
import cartopy.crs as ccrs
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
def plot_pie_inset(dataframe_pie,ilat_pie,ilon_pie,axis_main,width_local,alpha_local):
ax_sub= inset_axes(axis_main, width=width_local, height=width_local, loc=3,
bbox_to_anchor=(ilat_pie, ilon_pie),
bbox_transform=axis_main.transAxes,
borderpad=0.0)
wedges,texts= ax_sub.pie(dataframe_pie,colors=colors_dual)
for w in wedges:
w.set_linewidth(0.02)
w.set_alpha(alpha_local)
w.set_zorder(1)
plt.axis('equal')
# Put insets behind main axes
ax_sub.set_zorder(axis_main.get_zorder()-1)
colors_dual=['RosyBrown','LightBlue']
lat_list= np.arange(0.2,0.7,0.05)
fig= plt.figure()
ax_main= plt.subplot(1,1,1,projection=ccrs.PlateCarree())
ax_main.coastlines()
# set background patch invisible, such that axes becomes transparent
# since the GeoAxes from cartopy uses a different patch as background
# the following does not work
# ax_main.patch.set_visible(False)
# so we need to set the GeoAxes' background_patch invisible
ax_main.background_patch.set_visible(False)
for ilat in np.arange(len(lat_list)):
plot_pie_inset([75,25],lat_list[ilat],0.72,ax_main,0.2,0.9)
plt.show()
An alternative solution suggest by a colleague neglects to use the inset_axes but achieves a similar result. The main difference is that the coordinate system in this solution is in the original latitude/longitude coordinates rather than figure coordinates.
def plot_pie_direct(dataframe_pie,ilat_pie,ilon_pie,axis_main,width_local,alpha_local):
wedges,texts= ax_main.pie(dataframe_pie,colors=colors_aer_atm,radius=width_local)
for w in wedges:
w.set_linewidth(0.02) ## Reduce linewidth to near-zero
w.set_center((ilat_pie,ilon_pie))
w.set_zorder(0)
fig= plt.figure()
ax_main= plt.axes(projection=ccrs.PlateCarree())
ax_main.coastlines(zorder=3)
ax_main.set_global()
lim_x= ax_main.get_xlim()
lim_y= ax_main.get_ylim()
for ilat in np.arange(len(lat_list_trim)):
plot_pie_direct(frac_aer_atm_reshape_trim[:,ilat,ilon],x_val_pies[ilon],y_val_pies[ilat],ax_main,lat_list_diff_trim,0.9)
ax_main.coastlines(zorder=3)
ax_main.set_xlim(lim_x)
ax_main.set_ylim(lim_y)
plt.show()
Related
I would like to create a plot that shows only part of my data. But when a marker is on the edge of the axis I want to see the whole marker. It seems that both conditions are irreconcilable. Using clip_on=False :
import numpy as np
from matplotlib.pyplot import ion, plot, xlim, ylim
ion()
a = np.arange(11)
plot(a,'.', markersize=10, clip_on=False)
xlim(1,9)
ylim(1,9)
results in:
Here the whole marker on the edge is visible, but the datapoints outside the axis also.
Using clip_on=True (default) results in:
Here the edge markers are only partly visible, but there are no data points outside the axis.
Is there a way in matplotlib to reconcile both requirements, without modifyning the data?
is it ok to modify the scale? . if so this can accommodate
import numpy as np
from matplotlib.pyplot import ion, plot, xlim, ylim
ion()
a = np.arange(11)
print(a)
plot(a,'.', markersize=10, clip_on=True)
xlim(0,10)
ylim(0,10)
I'm trying to place a legend just above the ax in matplotlib using ax.legend(loc=(0, 1.1)); however, if I change the figure size from (5,5) to (5,10) the legend shows up at a different distance from the top edge of the plot.
Is there any way to reference the top edge of the plot and offset it a set distance from it?
Thanks
There is a constant distance between the legend bounding box and the axes by default. This is set via the borderaxespad parameter. This defaults to the rc value of rcParams["legend.borderaxespad"], which is usually set to 0.5 (in units of the fontsize).
So essentially you get the behaviour you're asking for for free. Mind however that you should specify the loc to the corner of the legend from which that padding is to be taken. I.e.
import numpy as np
import matplotlib.pyplot as plt
for figsize in [(5,4), (5,9)]:
fig, ax = plt.subplots(figsize=figsize)
ax.plot([1,2,3], label="label")
ax.legend(loc="lower left", bbox_to_anchor=(0,1))
plt.show()
For more detailed explanations on how to position legend outside the axes, see How to put the legend out of the plot. Also relevant: How to specify legend position in matplotlib in graph coordinates
Any ideas on how can I insert a scale bar in a map in matplotlib that shows the length scale? something like the one I have attached.
Or maybe any ideas on measuring and showing distances automatically (not drawing an arrow and writing the distance manually!)?
Thanks :)
There is a an already existing class for scalebars in matplotlib called AnchoredSizeBar. In the below example AnchoredSizeBar is used to add a scalebar to an image (or map over a 100x100 meter area of randomness).
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.anchored_artists import AnchoredSizeBar
import matplotlib.font_manager as fm
fontprops = fm.FontProperties(size=18)
fig, ax = plt.subplots()
ax.imshow(np.random.random((10,10)),extent=[0,100,0,100])
Extent defines the images max and min of the horizontal and vertical values.
scalebar = AnchoredSizeBar(ax.transData,
20, '20 m', 'lower center',
pad=0.1,
color='white',
frameon=False,
size_vertical=1,
fontproperties=fontprops)
ax.add_artist(scalebar)
The four first arguments to AnchoredSizeBar are the transformation object of the coordinate system, scalebar length, label and location. Further optional arguments change the layout. These are explained in the documentation.
ax.set_yticks([])
ax.set_xticks([])
This gives
I would try the matplotlib-scalebar package. (For something like your example c.)
Assuming you are plotting a map image with imshow or similar, and you know the pixel width/cell-size (the real-world equivalent size of one pixel on the map image), you can automatically create the scale bar:
This example is straight off the PyPi matplotlib-scalebar package page but here it is for completeness:
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
from matplotlib_scalebar.scalebar import ScaleBar
plt.figure()
image = plt.imread(cbook.get_sample_data('grace_hopper.png'))
plt.imshow(image)
scalebar = ScaleBar(0.2) # 1 pixel = 0.2 meter
plt.gca().add_artist(scalebar)
plt.show()
This is a slightly tricky one to explain. Basically, I want to make an inset plot and then utilize the convenience of mpl_toolkits.axes_grid1.inset_locator.mark_inset, but I want the data in the inset plot to be completely independent of the data in the parent axes.
Example code with the functions I'd like to use:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
data = np.random.normal(size=(2000,2000))
plt.imshow(data, origin='lower')
parent_axes = plt.gca()
ax2 = inset_axes(parent_axes, 1, 1)
ax2.plot([900,1100],[900,1100])
# I need more control over the position of the inset axes than is given by the inset_axes function
ip = InsetPosition(parent_axes,[0.7,0.7,0.3,0.3])
ax2.set_axes_locator(ip)
# I want to be able to control where the mark is connected to, independently of the data in the ax2.plot call
mark_inset(parent_axes, ax2, 2,4)
# plt.savefig('./inset_example.png')
plt.show()
The example code produces the following image:
So to sum up: The location of the blue box is entire controlled by the input data to ax2.plot(). I would like to manually place the blue box and enter whatever I want into ax2. Is this possible?
quick edit: to be clear, I understand why inset plots would have the data linked, as that's the most likely usage. So if there's a completely different way in matplotlib to accomplish this, do feel free to reply with that. However, I am trying to avoid manually placing boxes and lines to all of the axes I would place, as I need quite a few insets into a large image.
If I understand correctly, you want an arbitrarily scaled axis at a given position that looks like a zoomed inset, but has no connection to the inset marker's position.
Following your approach you can simply add another axes to the plot and position it at the same spot of the true inset, using the set_axes_locator(ip) function. Since this axis is drawn after the original inset, it will be on top of it and you'll only need to hide the tickmarks of the original plot to let it disappear completely (set_visible(False) does not work here, as it would hide the lines between the inset and the marker position).
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset, InsetPosition
data = np.random.normal(size=(200,200))
plt.imshow(data, origin='lower')
parent_axes = plt.gca()
ax2 = inset_axes(parent_axes, 1, 1)
ax2.plot([60,75],[90,110])
# hide the ticks of the linked axes
ax2.set_xticks([])
ax2.set_yticks([])
#add a new axes to the plot and plot whatever you like
ax3 = plt.gcf().add_axes([0,0,1,1])
ax3.plot([0,3,4], [2,3,1], marker=ur'$\u266B$' , markersize=30, linestyle="")
ax3.set_xlim([-1,5])
ax3.set_ylim([-1,5])
ip = InsetPosition(parent_axes,[0.7,0.7,0.3,0.3])
ax2.set_axes_locator(ip)
# set the new axes (ax3) to the position of the linked axes
ax3.set_axes_locator(ip)
# I want to be able to control where the mark is connected to, independently of the data in the ax2.plot call
mark_inset(parent_axes, ax2, 2,4)
plt.show()
FWIW, I came up with a hack that works.
In the source code for inset_locator, I added a version of mark_inset that takes another set of axes used to define the TransformedBbox:
def mark_inset_hack(parent_axes, inset_axes, hack_axes, loc1, loc2, **kwargs):
rect = TransformedBbox(hack_axes.viewLim, parent_axes.transData)
pp = BboxPatch(rect, **kwargs)
parent_axes.add_patch(pp)
p1 = BboxConnector(inset_axes.bbox, rect, loc1=loc1, **kwargs)
inset_axes.add_patch(p1)
p1.set_clip_on(False)
p2 = BboxConnector(inset_axes.bbox, rect, loc1=loc2, **kwargs)
inset_axes.add_patch(p2)
p2.set_clip_on(False)
return pp, p1, p2
Then in my original-post code I make an inset axis where I want the box to be, pass it to my hacked function, and make it invisible:
# location of desired axes
axdesire = inset_axes(parent_axes,1,1)
axdesire.plot([100,200],[100,200])
mark_inset_hack(parent_axes, ax2, axdesire, 2,4)
axdesire.set_visible(False)
Now I have a marked box at a different location in data units than the inset that I'm marking:
It is certainly a total hack, and at this point I'm not sure it's cleaner than simply drawing lines manually, but I think for a lot of insets this will keep things conceptually cleaner.
Other ideas are still welcome.
Any ideas on how can I insert a scale bar in a map in matplotlib that shows the length scale? something like the one I have attached.
Or maybe any ideas on measuring and showing distances automatically (not drawing an arrow and writing the distance manually!)?
Thanks :)
There is a an already existing class for scalebars in matplotlib called AnchoredSizeBar. In the below example AnchoredSizeBar is used to add a scalebar to an image (or map over a 100x100 meter area of randomness).
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.anchored_artists import AnchoredSizeBar
import matplotlib.font_manager as fm
fontprops = fm.FontProperties(size=18)
fig, ax = plt.subplots()
ax.imshow(np.random.random((10,10)),extent=[0,100,0,100])
Extent defines the images max and min of the horizontal and vertical values.
scalebar = AnchoredSizeBar(ax.transData,
20, '20 m', 'lower center',
pad=0.1,
color='white',
frameon=False,
size_vertical=1,
fontproperties=fontprops)
ax.add_artist(scalebar)
The four first arguments to AnchoredSizeBar are the transformation object of the coordinate system, scalebar length, label and location. Further optional arguments change the layout. These are explained in the documentation.
ax.set_yticks([])
ax.set_xticks([])
This gives
I would try the matplotlib-scalebar package. (For something like your example c.)
Assuming you are plotting a map image with imshow or similar, and you know the pixel width/cell-size (the real-world equivalent size of one pixel on the map image), you can automatically create the scale bar:
This example is straight off the PyPi matplotlib-scalebar package page but here it is for completeness:
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
from matplotlib_scalebar.scalebar import ScaleBar
plt.figure()
image = plt.imread(cbook.get_sample_data('grace_hopper.png'))
plt.imshow(image)
scalebar = ScaleBar(0.2) # 1 pixel = 0.2 meter
plt.gca().add_artist(scalebar)
plt.show()