Open changliao1025 opened 2 years ago
I have the beginning of an answer (which probably is not very general, but might help someone craft a better answer).
I'm plotting alternative black/white lines with black path effects between every tick location. It produces something like this:
https://gist.github.com/scottstanie/dff0d597e636440fb60b3c5443f70cae
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)
And here's the function itself
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(),
],
)
Thanks for this effort! I think this is close enough although I didn't test other SRS. I also believe this approach is similar to what the @idl-coyote uses in the above example.
My try based on @scottstanie .
The white frame seems not shown on my computer:
So I update code to deal with the frame linewidth self.spines["geo"].get_linewidth()
. Also, I add it as a method of GeoAxes
, so I can use it in a simple way ax.zebra_frame(...)
.
I use the code onto EquidistantConic projection. Unfortunatelly I don't know how to hide[^1] the out of extent part in Cartopy. Some gridline labels are also missing.
[^1]: Update: Maybe https://scitools.org.uk/cartopy/docs/latest/gallery/miscellanea/star_shaped_boundary.html will help to hide the out of extent part.
import itertools
from matplotlib.patheffects import Stroke, Normal
import numpy as np
import cartopy.mpl.geoaxes
def zebra_frame(self, lw=2, crs=None, zorder=None):
# Alternate black and white line segments
bws = itertools.cycle(["k", "w"])
self.spines["geo"].set_visible(False)
left, right, bottom, top = self.get_extent()
# xticks = sorted([left, *self.get_xticks(), right])
xticks = sorted([*self.get_xticks()])
xticks = np.unique(np.array(xticks))
# yticks = sorted([bottom, *self.get_yticks(), top])
yticks = sorted([*self.get_yticks()])
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 = [[yticks[0], yticks[0]], [yticks[-1], yticks[-1]]]
else:
xs = [[xticks[0], xticks[0]], [xticks[-1], xticks[-1]]]
ys = [[start, end], [start, end]]
# For first and last lines, used the "projecting" effect
capstyle = "butt" if idx not in (0, len(ticks) - 2) else "projecting"
for (xx, yy) in zip(xs, ys):
self.plot(xx, yy, color=bw, linewidth=max(0, lw - self.spines["geo"].get_linewidth()*2), clip_on=False,
transform=crs, zorder=zorder, solid_capstyle=capstyle,
# Add a black border to accentuate white segments
path_effects=[
Stroke(linewidth=lw, foreground="black"),
Normal(),
],
)
setattr(cartopy.mpl.geoaxes.GeoAxes, 'zebra_frame', zebra_frame)
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
crs = ccrs.EquidistantConic(central_longitude=-90)
fig = plt.figure(figsize=(8, 4))
ax = fig.add_subplot(
1, 1, 1, projection=crs)
ax.coastlines()
ax.set_extent((-125, -60, 0, 30), crs=ccrs.PlateCarree())
ax.set_xticks(np.arange(-120, -60+1, 5))
ax.set_yticks(np.arange(0, 30+1, 5))
ax.set_axis_off()
ax.gridlines(draw_labels=True, dms=True, linestyle='--',
# # x_inline=True, y_inline=True,
xlocs=np.arange(-120, -60+1, 15), ylocs=np.arange(0, 30+1, 15))
ax.zebra_frame(lw=5, crs=ccrs.PlateCarree(), zorder=3)
plt.show()
Many thanks @wohenbushuang. You may also need to import numpy as np in the second script file.
Based on @wohenbushuang version, I added an option to allow the frame to be plotted using the map extent instead of following the latlon paths:
import itertools
from matplotlib.patheffects import Stroke, Normal
import numpy as np
import cartopy.mpl.geoaxes
def zebra_frame(self, lw=3, crs=None, zorder=None, iFlag_outer_frame_in = None):
# Alternate black and white line segments
bws = itertools.cycle(["k", "w"])
self.spines["geo"].set_visible(False)
if iFlag_outer_frame_in is not None:
#get the map spatial reference
left, right, bottom, top = self.get_extent()
crs_map = self.projection
xticks = np.arange(left, right+(right-left)/9, (right-left)/8)
yticks = np.arange(bottom, top+(top-bottom)/9, (top-bottom)/8)
#check spatial reference are the same
pass
else:
crs_map = crs
xticks = sorted([*self.get_xticks()])
xticks = np.unique(np.array(xticks))
yticks = sorted([*self.get_yticks()])
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 = [[yticks[0], yticks[0]], [yticks[-1], yticks[-1]]]
else:
xs = [[xticks[0], xticks[0]], [xticks[-1], xticks[-1]]]
ys = [[start, end], [start, end]]
# For first and last lines, used the "projecting" effect
capstyle = "butt" if idx not in (0, len(ticks) - 2) else "projecting"
for (xx, yy) in zip(xs, ys):
self.plot(xx, yy, color=bw, linewidth=max(0, lw - self.spines["geo"].get_linewidth()*2), clip_on=False,
transform=crs_map, zorder=zorder, solid_capstyle=capstyle,
# Add a black border to accentuate white segments
path_effects=[
Stroke(linewidth=lw, foreground="black"),
Normal(),
],
)
setattr(cartopy.mpl.geoaxes.GeoAxes, 'zebra_frame', zebra_frame)
This is the result:
Thank you for all the contributions. @wohenbushuang and @scottstanie
This looks great! It sounds like (https://stackoverflow.com/questions/57313303/how-to-plot-zebra-style-axis-in-matplotlib) you will make it into a pull request?
Problem Zebra style map frame is a nice feature supported in many map applications including ArcGIS, ENVI/IDL, GMT. http://www.idlcoyote.com/idldoc/cg/cgdrawshapes.html https://gmt-tutorials.org/en/making_first_map.html
Proposed solution Add a flag to turn on this feature if plotting a map with a map structure.
Additional context and prior art https://stackoverflow.com/questions/57313303/how-to-plot-zebra-style-axis-in-matplotlib