How to plot zebra style axis in matplotlib - python

I want plot the axis in a zebra style similar to this:
Below is my code:
import matplotlib.pyplot as plt
import cartopy.io.shapereader as shpreader
import cartopy.crs as ccrs
from cartopy.feature import ShapelyFeature
fig, ax = plt.figure(figsize=(12,9), dpi=150 )
sFilename_shapefile = './some_shape.shp'
pShapeReader = shpreader.Reader(sFilename_shapefile)
pProjection_map = ccrs.PlateCarree()
aShapeFeature = ShapelyFeature(pShapeReader.geometries(),
pProjection_map, facecolor='grey', edgecolor='grey',
linewidth=0.5)
ax.add_feature(aShapeFeature, zorder = 4)
plt.show()
What I got is like this:

I've got a hacky solution that's working for my purposes:
The example usage:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
crs = ccrs.PlateCarree()
fig = plt.figure(figsize=(5, 2))
ax = fig.add_subplot(projection=crs)
ax.coastlines()
ax.set_extent((-125, -85, 22, 42))
ax.set_xticks((-120, -110, -100, -90))
ax.set_yticks((25, 30, 35, 40))
add_zebra_frame(ax, crs=crs)
I've put the frame in a function for now. It likely will not work for many polar-type projections that mix lat/lon ticks, and right now it doesn't work that well if you don't specify which tick marks you want (I'm still unclear how Cartopy picks the default ticks).
https://gist.github.com/scottstanie/dff0d597e636440fb60b3c5443f70cae
Basically all I'm doing is turning off the spines and plotting an alternating black/white line between each of the xticks/yticks.
import itertools
import matplotlib.patheffects as pe
import numpy as np
def add_zebra_frame(ax, lw=2, crs="pcarree", zorder=None):
ax.spines["geo"].set_visible(False)
left, right, bot, top = ax.get_extent()
# Alternate black and white line segments
bws = itertools.cycle(["k", "white"])
xticks = sorted([left, *ax.get_xticks(), right])
xticks = np.unique(np.array(xticks))
yticks = sorted([bot, *ax.get_yticks(), top])
yticks = np.unique(np.array(yticks))
for ticks, which in zip([xticks, yticks], ["lon", "lat"]):
for idx, (start, end) in enumerate(zip(ticks, ticks[1:])):
bw = next(bws)
if which == "lon":
xs = [[start, end], [start, end]]
ys = [[bot, bot], [top, top]]
else:
xs = [[left, left], [right, right]]
ys = [[start, end], [start, end]]
# For first and lastlines, used the "projecting" effect
capstyle = "butt" if idx not in (0, len(ticks) - 2) else "projecting"
for (xx, yy) in zip(xs, ys):
ax.plot(
xx,
yy,
color=bw,
linewidth=lw,
clip_on=False,
transform=crs,
zorder=zorder,
solid_capstyle=capstyle,
# Add a black border to accentuate white segments
path_effects=[
pe.Stroke(linewidth=lw + 1, foreground="black"),
pe.Normal(),
],
)

Related

Piece together a projection using cartopy

I was trying to get a map projection using cartopy in python and it wasn't made so I'm trying to piece it together using to subplots with the following code:
fig = plt.figure(figsize =(25,13),facecolor='white')
gs = fig.add_gridspec(1,2,width_ratios=[4,2],height_ratios = [1], hspace=0.2,wspace=.0)
ax1=fig.add_subplot(gs[0,0],projection=ccrs.PlateCarree())
ax2=fig.add_subplot(gs[0,1],projection=ccrs.PlateCarree())
ax2.set_extent([-180,0,-90,90])
ax1.set_extent([-180,180,-90,90])
ax1.add_feature(cfeature.LAND, color = 'lightgray')
ax2.add_feature(cfeature.LAND, color = 'lightgray')
ax1.add_feature(cfeature.COASTLINE)
ax2.add_feature(cfeature.COASTLINE)
and I get the right projection I was looking for, however I am trying to remove the line between the two subplots and I keep getting issues, any suggestions?
Your question is a challenge as it is uncommon to plot a map with longitude extent greater than 360 degrees. What you have done is already a good achievement. What I will do just to finish your work.
Here is the code that produces the plot you need.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
#from shapely.geometry import Point, Polygon
import cartopy.feature as cfeature
import matplotlib.transforms as transforms
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
fig = plt.figure(figsize =(25,9.5), facecolor='white')
gs = fig.add_gridspec(1, 2, width_ratios=[4,2], height_ratios = [1], hspace=0.2, wspace=.0)
proj = ccrs.PlateCarree(central_longitude=0)
ax1=fig.add_subplot( gs[0,0], projection=proj )
ax2=fig.add_subplot( gs[0,1], projection=proj )
ax1.set_extent([-179.9, 180, -90, 90]) #Tricky, -180 not works!
ax2.set_extent([-179.9, 0, -90, 90])
ax1.add_feature(cfeature.LAND, color = 'lightgray')
ax2.add_feature(cfeature.LAND, color = 'lightgray')
ax1.add_feature(cfeature.COASTLINE)
ax2.add_feature(cfeature.COASTLINE)
# Set color of ax2's boundaries
# If set 'white' the gridline at that position will be gone!
ax2.outline_patch.set_edgecolor('lightgray') # set color to match other gridlines
# Draw 3 sides boundaries of ax2
# ------------------------------
# Define a `transformation`
# Signature: blended_transform_factory(x_transform, y_transform)
# the y coords of this transformation are data (as is = ax.transData)
# but the x coord are axes coordinate (0 to 1, ax.transAxes)
transAD = transforms.blended_transform_factory(ax2.transAxes, ax2.transData)
# Plot 3 lines around extents of ax2
# Color is intentionally set as 'red'
# You need to change it to 'black' for your work
ax2.plot([0.996, 0.996], [-90, 90], color='red', lw=2, transform=transAD)
ax2.plot([0.0, 0.996], [-90, -90], color='red', lw=2, transform=transAD)
ax2.plot([0.0, 0.996], [89.6, 89.6], color='red', lw=2, transform=transAD)
gl1 = ax1.gridlines(ccrs.PlateCarree(),
xlocs=range(-180,181,20),
ylocs=range(-90,90,10),
linestyle='-',
y_inline=False, x_inline=False,
color='b', alpha=0.6, linewidth=0.25, draw_labels=True)
gl1.xformatter = LONGITUDE_FORMATTER
gl1.yformatter = LATITUDE_FORMATTER
gl1.right_labels = False
gl2 = ax2.gridlines(ccrs.PlateCarree(),
xlocs=range(-180,180,20),
ylocs=range(-90,90,10),
linestyle='-',
y_inline=False, x_inline=False,
color='b', alpha=0.6, linewidth=0.25, draw_labels=True)
gl2.xformatter = LONGITUDE_FORMATTER
gl2.yformatter = LATITUDE_FORMATTER
gl2.left_labels = False

Center 3D bars on the given positions in matplotlib

Consider a 3D bar plot with custom grid lines:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.ticker import MultipleLocator
# This import registers the 3D projection, but is otherwise unused.
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import
fig = plt.figure(figsize=(20, 10))
ax = fig.add_subplot(111, projection='3d')
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
ax.zaxis.set_major_locator(MultipleLocator(2))
nx = 10
ny = 10
colors = cm.tab20(np.linspace(0, 1, nx))
width = depth = 0.1
for x in np.arange(nx):
for y in np.arange(ny):
ax.bar3d(x, y, 0, width, depth, x+y, shade=False, color = colors[x], edgecolor = 'black')
plt.show()
How can I place the bars so that the bars are centered where the grid lines cross each other in the xy plane?
I'm thinking about something like
ax.bar3d(x+0.5*depth, y+0.5*width, ...)
only it is not clear to me what the offset is that matplotlib uses. It should work for all depth and width values.
For 2D bar plots there is an argument for this, align = 'center', but it doesn't seem to work for 3D.
What looks to you as a shift in coordinates is really just the projection in combination with the margins of the axes. Hence even if the bars are correctly positionned in their center they look offset and that offset is dependent on the axes size, viewing angle etc.
The solution to this is in principle given in this Q&A:
Removing axes margins in 3D plot
You would center the bars by subtracting half of their width and add a patch to remove the margin of the zaxis. Then setting the lower z limit to 0 pins the bars to the grid and makes them look centered for any viewing angle.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.ticker import MultipleLocator
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.axis3d import Axis
def _get_coord_info_new(self, renderer):
mins, maxs, cs, deltas, tc, highs = self._get_coord_info_old(renderer)
correction = deltas * [0,0,1.0/4]
mins += correction
maxs -= correction
return mins, maxs, cs, deltas, tc, highs
if not hasattr(Axis, "_get_coord_info_old"):
Axis._get_coord_info_old = Axis._get_coord_info
Axis._get_coord_info = _get_coord_info_new
fig = plt.figure(figsize=(20, 10))
ax = fig.add_subplot(111, projection='3d')
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
ax.zaxis.set_major_locator(MultipleLocator(2))
nx = 10
ny = 10
colors = cm.tab20(np.linspace(0, 1, nx))
width = depth = 0.1
for x in np.arange(nx):
for y in np.arange(ny):
ax.bar3d(x-width/2., y-depth/2., 0, width, depth, x+y, shade=False,
color = colors[x], edgecolor = 'black')
ax.set_zlim(0,None)
plt.show()

Center matplotlib colormap on a specific value

I'm making plots using matplotlib colormap "seismic" and would like to have the white color centered on 0. When I run my script with no changes, white falls from 0 to -10. I tried then setting vmin=-50, vmax=50 but I completely lose the white in that case. Any suggestions on how to accomplish that?
from netCDF4 import Dataset as NetCDFFile
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.basemap import Basemap
nc = NetCDFFile('myfile.nc')
lat = nc.variables['lat'][:]
lon = nc.variables['lon'][:]
time = nc.variables['time'][:]
hgt = nc.variables['hgt'][:]
map = Basemap(llcrnrlon=180.,llcrnrlat=0.,urcrnrlon=320.,urcrnrlat=80.)
lons,lats = np.meshgrid(lon,lat)
x,y = map(lons,lats)
cs = map.contourf(x,y,hgt[0],cmap='seismic')
cbar = plt.colorbar(cs, orientation='horizontal', shrink=0.5,
cmap='seismic')
cbar.set_label('500mb Geopotential Height Anomalies(m)')
map.drawcoastlines()
map.drawparallels(np.arange(20,80,20),labels=[1,1,0,0], linewidth=0.5)
map.drawmeridians(np.arange(200,320,20),labels=[0,0,0,1], linewidth=0.5)
plt.show()`
Plot with defaults
Plot with vmin, vmax set
You can set the levels you want to show manually. As long as you have the same spacing of intervals to the left and to the right of zero this works nicely.
levels = [-50,-40,-30,-20,-10,10,20,30,40,50]
ax.contourf(X,Y,Z, levels)
Example:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-6.3,6.3)
y = np.linspace(-3.1,3.1)
X,Y = np.meshgrid(x,y)
Z = -np.cos(X)*np.cos(Y)*45
levels = [-50,-40,-30,-20,-10,10,20,30,40,50]
fig, ax = plt.subplots(figsize=(4,2))
cont = ax.contourf(X,Y,Z,levels, cmap="seismic")
fig.colorbar(cont, orientation="horizontal")
plt.show()
Or, if you want the colorbar to be proportional to the data,
fig.colorbar(cont, orientation="horizontal", spacing="proportional")
If levels are unequal, you need to specify vmin and vmax.
levels = [-50,-40,-30,-20,-10,10,30,50,80,100]
cont = ax.contourf(X,Y,Z,levels, cmap="seismic", vmin=-50, vmax=50)
The disadvantage is that you loose resolution, hence you may use a BoundaryNorm to select equally spaced colors for unequally spaced labels.
import matplotlib.pyplot as plt
import matplotlib.colors
import numpy as np
x = np.linspace(-6.3,6.3)
y = np.linspace(-3.1,3.1)
X,Y = np.meshgrid(x,y)
Z = -np.cos(X)*np.cos(Y)*45
levels = [-50,-40,-30,-20,-10,10,30,50,80,100]
norm = matplotlib.colors.BoundaryNorm(levels, len(levels)-1)
fig, ax = plt.subplots(figsize=(4,2))
cont = ax.contourf(X,Y,Z,levels,cmap=plt.get_cmap("seismic",len(levels)-1), norm=norm)
fig.colorbar(cont, orientation="horizontal")
plt.show()
To change the ticklabels on the colorbar so something other than the levels or in case they are too dence you may use the ticks argument.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-6.3,6.3)
y = np.linspace(-3.1,3.1)
X,Y = np.meshgrid(x,y)
Z = -np.cos(X)*np.cos(Y)*45
levels = np.arange(-45,50,5)
levels = levels[levels!=0]
ticks=np.arange(-40,50,10)
fig, ax = plt.subplots(figsize=(4,2))
cont = ax.contourf(X,Y,Z,levels,cmap="seismic", spacing="proportional")
fig.colorbar(cont, orientation="horizontal", ticks=ticks, spacing="proportional")
plt.show()

cmap for use with line plot

I have a list of color values (in either of the formats: hex ('#ffffff') or rgb (255,255,255) if that helps). These colors correspond explicitly with the line segment between points. Currently I plot a line as a collection of line segments via:
import matplotlib.pyplot as plt
import itertools
colors = itertools.cycle('#ffffff', '#ffffff', '#ff0320', '#452143', ...)
t = (0, 1, 2, 3, ...)
var1 = (43, 15, 25, 9, ...)
ax = plt.subplot2grid((3,1), (0,0), colspan=3, rowspan=1)
ps = [(t,var1) for (t,var1) in zip(t, val)]
for start, end in zip(ps[:-1], ps[1:]):
t, var1 = zip(start, end)
c = next(colors)
ax.plot(t, var1, color=c)
However since I have a color for each point I would much prefer to set a cmap for the plot. How might I accomplish converting a list of colors into a cmap which I can use when plotting a line?
As tcaswell says, use a LineCollection for this:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.collections import LineCollection
# a random walk
xy = np.cumsum(np.random.randn(1000, 2), axis=0)
z = np.linspace(0, 1, 1000)
lc = LineCollection(zip(xy[:-1], xy[1:]), array=z, cmap=plt.cm.hsv)
fig, ax = plt.subplots(1, 1)
ax.add_collection(lc)
ax.margins(0.1)
plt.show()

How to draw a filled arc in matplotlib

In matplotlib, I would like draw an filled arc which looks like this:
The following code results in an unfilled line arc:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
fg, ax = plt.subplots(1, 1)
pac = mpatches.Arc([0, -2.5], 5, 5, angle=0, theta1=45, theta2=135)
ax.add_patch(pac)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.canvas.draw()
The documentation says that filled arcs are not possible.
What would be the best way to draw one?
#jeanrjc's solution almost gets you there, but it adds a completely unnecessary white triangle, which will hide other objects as well (see figure below, version 1).
This is a simpler approach, which only adds a polygon of the arc:
Basically we create a series of points (points) along the edge of the circle (from theta1 to theta2). This is already enough, as we can set the close flag in the Polygon constructor which will add the line from the last to the first point (creating a closed arc).
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
def arc_patch(center, radius, theta1, theta2, ax=None, resolution=50, **kwargs):
# make sure ax is not empty
if ax is None:
ax = plt.gca()
# generate the points
theta = np.linspace(np.radians(theta1), np.radians(theta2), resolution)
points = np.vstack((radius*np.cos(theta) + center[0],
radius*np.sin(theta) + center[1]))
# build the polygon and add it to the axes
poly = mpatches.Polygon(points.T, closed=True, **kwargs)
ax.add_patch(poly)
return poly
And then we apply it:
fig, ax = plt.subplots(1,2)
# #jeanrjc solution, which might hide other objects in your plot
ax[0].plot([-1,1],[1,-1], 'r', zorder = -10)
filled_arc((0.,0.3), 1, 90, 180, ax[0], 'blue')
ax[0].set_title('version 1')
# simpler approach, which really is just the arc
ax[1].plot([-1,1],[1,-1], 'r', zorder = -10)
arc_patch((0.,0.3), 1, 90, 180, ax=ax[1], fill=True, color='blue')
ax[1].set_title('version 2')
# axis settings
for a in ax:
a.set_aspect('equal')
a.set_xlim(-1.5, 1.5)
a.set_ylim(-1.5, 1.5)
plt.show()
Result (version 2):
You can use fill_between to achieve this
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
fg, ax = plt.subplots(1, 1)
r=2.
yoff=-1
x=np.arange(-1.,1.05,0.05)
y=np.sqrt(r-x**2)+yoff
ax.fill_between(x,y,0)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.canvas.draw()
Play around with r and yoff to move the arc
EDIT:
OK, so you want to be able to plot arbitrary angles? You just need to find the equation of the chord, rather than using a flat line like above. Here's a function to do just that:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
fg, ax = plt.subplots(1, 1)
col='rgbkmcyk'
def filled_arc(center,r,theta1,theta2):
# Range of angles
phi=np.linspace(theta1,theta2,100)
# x values
x=center[0]+r*np.sin(np.radians(phi))
# y values. need to correct for negative values in range theta=90--270
yy = np.sqrt(r-x**2)
yy = [-yy[i] if phi[i] > 90 and phi[i] < 270 else yy[i] for i in range(len(yy))]
y = center[1] + np.array(yy)
# Equation of the chord
m=(y[-1]-y[0])/(x[-1]-x[0])
c=y[0]-m*x[0]
y2=m*x+c
# Plot the filled arc
ax.fill_between(x,y,y2,color=col[theta1/45])
# Lets plot a whole range of arcs
for i in [0,45,90,135,180,225,270,315]:
filled_arc([0,0],1,i,i+45)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.savefig('filled_arc.png')
And here's the output:
Here's a simpler workaround. Use the hatch argument in your mpatches.Arc command. If you repeat symbols with the hatch argument it increases the density of the patterning. I find that if you use 6 dashes, '-', or 6 dots, '.' (others probably also work), then it solidly fills in the arc as desired. When I run this
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
plt.axes()
pac = mpatches.Arc([0, -2.5], 5, 5, 45, theta1=45, theta2=135, hatch = '......')
plt.gca().add_patch(pac)
pac.set_color('cyan')
plt.axis('equal')
plt.show()
I get this:
Arc filled with dense dot hatch and rotated 45 degrees just for show
You can draw a wedge, and then hide part of it with a triangle:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
def filled_arc(center, radius, theta1, theta2, ax, color):
circ = mpatches.Wedge(center, radius, theta1, theta2, fill=True, color=color)
pt1 = (radius * (np.cos(theta1*np.pi/180.)) + center[0],
radius * (np.sin(theta1*np.pi/180.)) + center[1])
pt2 = (radius * (np.cos(theta2*np.pi/180.)) + center[0],
radius * (np.sin(theta2*np.pi/180.)) + center[1])
pt3 = center
pol = mpatches.Polygon([pt1, pt2, pt3], color=ax.get_axis_bgcolor(),
ec=ax.get_axis_bgcolor(), lw=2 )
ax.add_patch(circ)
ax.add_patch(pol)
and then you can call it:
fig, ax = plt.subplots(1,2)
filled_arc((0,0), 1, 45, 135, ax[0], "blue")
filled_arc((0,0), 1, 0, 40, ax[1], "blue")
and you get:
or:
fig, ax = plt.subplots(1, 1)
for i in range(0,360,45):
filled_arc((0,0), 1, i, i+45, ax, plt.cm.jet(i))
and you get:
HTH
The command ax.get_axis_bgcolor() needs to be replaced by ax.get_fc() for newer matplotlib.

Categories