How to insert scale bar in a map in matplotlib - python

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()

Related

Scale legend matplotlib [duplicate]

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()

Cartopy coastlines hidden by inset_axes use of Axes.pie

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()

Plot a (polar) color wheel based on a colormap using Python/Matplotlib

I am trying to create a color wheel in Python, preferably using Matplotlib. The following works OK:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
xval = np.arange(0, 2*pi, 0.01)
yval = np.ones_like(xval)
colormap = plt.get_cmap('hsv')
norm = mpl.colors.Normalize(0.0, 2*np.pi)
ax = plt.subplot(1, 1, 1, polar=True)
ax.scatter(xval, yval, c=xval, s=300, cmap=colormap, norm=norm, linewidths=0)
ax.set_yticks([])
However, this attempt has two serious drawbacks.
First, when saving the resulting figure as a vector (figure_1.svg), the color wheel consists (as expected) of 621 different shapes, corresponding to the different (x,y) values being plotted. Although the result looks like a circle, it isn't really. I would greatly prefer to use an actual circle, defined by a few path points and Bezier curves between them, as in e.g. matplotlib.patches.Circle. This seems to me the 'proper' way of doing it, and the result would look nicer (no banding, better gradient, better anti-aliasing).
Second (relatedly), the final plotted markers (the last few before 2*pi) overlap the first few. It's very hard to see in the pixel rendering, but if you zoom in on the vector-based rendering you can clearly see the last disc overlap the first few.
I tried using different markers (. or |), but none of them go around the second issue.
Bottom line: can I draw a circle in Python/Matplotlib which is defined in the proper vector/Bezier curve way, and which has an edge color defined according to a colormap (or, failing that, an arbitrary color gradient)?
One way I have found is to produce a colormap and then project it onto a polar axis. Here is a working example - it includes a nasty hack, though (clearly commented). I'm sure there's a way to either adjust limits or (harder) write your own Transform to get around it, but I haven't quite managed that yet. I thought the bounds on the call to Normalize would do that, but apparently not.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
import matplotlib as mpl
fig = plt.figure()
display_axes = fig.add_axes([0.1,0.1,0.8,0.8], projection='polar')
display_axes._direction = 2*np.pi ## This is a nasty hack - using the hidden field to
## multiply the values such that 1 become 2*pi
## this field is supposed to take values 1 or -1 only!!
norm = mpl.colors.Normalize(0.0, 2*np.pi)
# Plot the colorbar onto the polar axis
# note - use orientation horizontal so that the gradient goes around
# the wheel rather than centre out
quant_steps = 2056
cb = mpl.colorbar.ColorbarBase(display_axes, cmap=cm.get_cmap('hsv',quant_steps),
norm=norm,
orientation='horizontal')
# aesthetics - get rid of border and axis labels
cb.outline.set_visible(False)
display_axes.set_axis_off()
plt.show() # Replace with plt.savefig if you want to save a file
This produces
If you want a ring rather than a wheel, use this before plt.show() or plt.savefig
display_axes.set_rlim([-1,1])
This gives
As per #EelkeSpaak in comments - if you save the graphic as an SVG as per the OP, here is a tip for working with the resulting graphic: The little elements of the resulting SVG image are touching and non-overlapping. This leads to faint grey lines in some renderers (Inkscape, Adobe Reader, probably not in print). A simple solution to this is to apply a small (e.g. 120%) scaling to each of the individual gradient elements, using e.g. Inkscape or Illustrator. Note you'll have to apply the transform to each element separately (the mentioned software provides functionality to do this automatically), rather than to the whole drawing, otherwise it has no effect.
I just needed to make a color wheel and decided to update rsnape's solution to be compatible with matplotlib 2.1. Rather than place a colorbar object on an axis, you can instead plot a polar colored mesh on a polar plot.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
import matplotlib as mpl
# If displaying in a Jupyter notebook:
# %matplotlib inline
# Generate a figure with a polar projection
fg = plt.figure(figsize=(8,8))
ax = fg.add_axes([0.1,0.1,0.8,0.8], projection='polar')
# Define colormap normalization for 0 to 2*pi
norm = mpl.colors.Normalize(0, 2*np.pi)
# Plot a color mesh on the polar plot
# with the color set by the angle
n = 200 #the number of secants for the mesh
t = np.linspace(0,2*np.pi,n) #theta values
r = np.linspace(.6,1,2) #radius values change 0.6 to 0 for full circle
rg, tg = np.meshgrid(r,t) #create a r,theta meshgrid
c = tg #define color values as theta value
im = ax.pcolormesh(t, r, c.T,norm=norm) #plot the colormesh on axis with colormap
ax.set_yticklabels([]) #turn of radial tick labels (yticks)
ax.tick_params(pad=15,labelsize=24) #cosmetic changes to tick labels
ax.spines['polar'].set_visible(False) #turn off the axis spine.
It gives this:

Matplotlib imshow() stretch to "fit width"

I've got an image, and a measure associated with each column of its pixels. I'm using pyplot to create a figure with the image on top, and a plot of the column measurements below. I'm using something like this:
import numpy as np
import matplotlib.pyplot as plt
A = np.random.rand(34*52).reshape(34,52)
means = np.average(A,axis=0)
plt.figure()
plt.subplot(2,1,1)
plt.imshow(A, interpolation='nearest' )
plt.subplot(2,1,2)
plt.plot(means)
plt.show()
How can I stretch the image's width to the match that of the plots. That way, when looking at the measurements in the plot, the souce pixels will be in a column directly above it.
Turns out that it's as simple as giving aspect='auto' to the imshow call.
plt.imshow(A, interpolation='nearest', aspect='auto')

How to insert a small image on the corner of a plot with matplotlib?

What I want is really simple: I have a small image file called "logo.png" that I want to display on the upper left corner of my plots. But you can't find any example of that in the matplotlib examples gallery.
I'm using django, and my code is something like this:
def get_bars(request)
...
fig = Figure(facecolor='#F0F0F0',figsize=(4.6,4))
...
ax1 = fig.add_subplot(111,ylabel="Valeur",xlabel="Code",autoscale_on=True)
ax1.bar(ind,values,width=width, color='#FFCC00',edgecolor='#B33600',linewidth=1)
...
canvas = FigureCanvas(fig)
response = HttpResponse(content_type='image/png')
canvas.print_png(response)
return response
If you want the image at the corner of your actual figure (rather than the corner of your axis), look into figimage.
Perhaps something like this? (using PIL to read the image):
import matplotlib.pyplot as plt
import Image
import numpy as np
im = Image.open('/home/jofer/logo.png')
height = im.size[1]
# We need a float array between 0-1, rather than
# a uint8 array between 0-255
im = np.array(im).astype(np.float) / 255
fig = plt.figure()
plt.plot(np.arange(10), 4 * np.arange(10))
# With newer (1.0) versions of matplotlib, you can
# use the "zorder" kwarg to make the image overlay
# the plot, rather than hide behind it... (e.g. zorder=10)
fig.figimage(im, 0, fig.bbox.ymax - height)
# (Saving with the same dpi as the screen default to
# avoid displacing the logo image)
fig.savefig('/home/jofer/temp.png', dpi=80)
plt.show()
Another option, if you'd like to have the image be a fixed fraction of the figure's width/height is to create a "dummy" axes and place the image in it with imshow. This way the image's size and position is independent of DPI and the figure's absolute size:
import matplotlib.pyplot as plt
from matplotlib.cbook import get_sample_data
im = plt.imread(get_sample_data('grace_hopper.jpg'))
fig, ax = plt.subplots()
ax.plot(range(10))
# Place the image in the upper-right corner of the figure
#--------------------------------------------------------
# We're specifying the position and size in _figure_ coordinates, so the image
# will shrink/grow as the figure is resized. Remove "zorder=-1" to place the
# image in front of the axes.
newax = fig.add_axes([0.8, 0.8, 0.2, 0.2], anchor='NE', zorder=-1)
newax.imshow(im)
newax.axis('off')
plt.show()
There is now a much easier way, using the new inset_axes command (matplotlib >3.0 required).
This command allows one to define a new set of axes as a child of an existing axes object. The advantage of this is that you can define your inset axes in whatever units you please, like axes fraction or data coordinates, using the appropriate transform expression.
So here's a code example:
# Imports
import matplotlib.pyplot as plt
import matplotlib as mpl
# read image file
with mpl.cbook.get_sample_data(r"C:\path\to\file\image.png") as file:
arr_image = plt.imread(file, format='png')
# Draw image
axin = ax.inset_axes([105,-145,40,40],transform=ax.transData) # create new inset axes in data coordinates
axin.imshow(arr_image)
axin.axis('off')
The advantage of this method is that your image will scale automatically as your axes get rescaled!

Categories

Resources