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

Add the zebra frame feature #2379

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions examples/miscellanea/zebra_frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Gridlines and tick labels
-------------------------

These examples demonstrate how to quickly add longitude
and latitude gridlines and tick labels on a non-rectangular projection.

As you can see on the first example,
longitude labels may be drawn on left and right sides,
and latitude labels may be drawn on bottom and top sides.
Thanks to the ``dms`` keyword, minutes are used when appropriate
to display fractions of degree.

In the second example, labels are still drawn at the map edges
despite its complexity, and some others are also drawn within the map
boundary.

In the third example, labels are drawn only on the left and bottom sides.
"""
import matplotlib.pyplot as plt

import cartopy.crs as ccrs


def main():



plt.figure(figsize=(6.9228, 3))
ax1 = plt.axes(projection=ccrs.InterruptedGoodeHomolosine())
ax1.coastlines(resolution='110m')
ax1.gridlines(draw_labels=True)
ax1.zebra_frame(use_extent=True)

plt.figure(figsize=(7, 3))
ax2 = plt.axes(projection=ccrs.PlateCarree())
ax2.coastlines(resolution='110m')
gl = ax2.gridlines(draw_labels=True)
gl.top_labels = False
gl.right_labels = False
ax2.zebra_frame(colors=['blue', 'green'])
plt.show()
print('Done')


if __name__ == '__main__':
main()
91 changes: 91 additions & 0 deletions lib/cartopy/mpl/geoaxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import collections
import contextlib
import functools
import itertools
import json
import os
from pathlib import Path
Expand All @@ -29,6 +30,7 @@
from matplotlib.image import imread
import matplotlib.patches as mpatches
import matplotlib.path as mpath
from matplotlib.patheffects import Normal, Stroke
import matplotlib.spines as mspines
import matplotlib.transforms as mtransforms
import numpy as np
Expand Down Expand Up @@ -1514,6 +1516,95 @@ def gridlines(self, crs=None, draw_labels=False,
self.add_artist(gl)
return gl

def zebra_frame(self, lw=3, colors= None, crs=None, zorder=None, use_ticks = False, use_extent = True, nrow=8, ncolumn=8):
"""
Author: Chang Liao ([email protected])
Automatically add zebra frame to the axes, in the given coordinate
system, at draw time.

Parameters
----------
lw: optional
The line width of the zebra frame.
colors: optional
The colors of the zebra frame, a list of two colors.
crs: optional
The :class:`cartopy._crs.CRS` defining the coordinate system in
which gridlines are drawn.
Defaults to :class:`cartopy.crs.PlateCarree`.
zorder: optional
The zorder of the zebra frame.
use_ticks: optional
If True, the zebra frame will follow the ticks.
use_extent: optional
If True, the zebra frame will follow the map extent.
Returns
-------
Notes
-----
Details: https://github.com/SciTools/cartopy/issues/1830

"""
# Alternate black and white line segments
if colors is None:
bws = itertools.cycle(["k", "w"])
else:
#check colors size
nColor = len(colors)
if nColor !=2 :
raise ValueError("The colors must be a list of two colors.")
for color in colors:
if not matplotlib.colors.is_color_like(color):
raise ValueError(f"{color} is not a valid color.")

bws = itertools.cycle(colors)

self.spines["geo"].set_visible(False)
#by default, the extent will be used
if use_extent is True:
left, right, bottom, top = self.get_extent()
crs_map = self.projection
xticks = np.arange(left, right+(right-left)/(ncolumn+1), (right-left)/ncolumn)
yticks = np.arange(bottom, top+(top-bottom)/(nrow+1), (top-bottom)/nrow)
else:
if use_ticks is True:
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))
#check ticks size
if len(xticks) < 2 or len(yticks) < 2:
raise ValueError("The ticks must have at least two values.")
else:
#throw an error that one option must be true
raise ValueError("One of the options 'use_extent' or 'use_ticks' must be set to True.")


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

return

def _gen_axes_patch(self):
return _ViewClippedPathPatch(self)

Expand Down
Loading