Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zebra style axis for map #1830

Open
changliao1025 opened this issue Aug 25, 2021 · 6 comments · May be fixed by #2379
Open

Zebra style axis for map #1830

changliao1025 opened this issue Aug 25, 2021 · 6 comments · May be fixed by #2379

Comments

@changliao1025
Copy link

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

@scottstanie
Copy link

scottstanie commented Apr 20, 2022

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:
image

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

@changliao1025
Copy link
Author

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.

@wohenbushuang
Copy link

wohenbushuang commented Jul 20, 2022

My try based on @scottstanie .

The white frame seems not shown on my computer:
image

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 hide1 the out of extent part in Cartopy. Some gridline labels are also missing.

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

image

Footnotes

  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.

@nguyenquangchien
Copy link

Many thanks @wohenbushuang. You may also need to import numpy as np in the second script file.

@changliao1025
Copy link
Author

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:
201912

Thank you for all the contributions. @wohenbushuang and @scottstanie

@lgolston
Copy link
Contributor

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?

@lgolston lgolston linked a pull request May 3, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants