最近,我一直在检查 matplotlib.offset 类(即:AuxTransformBox、VPacker、HPacker、TextArea)的适用性。
在我的研究中,我证实有时 Packer 类(VPacker 和 HPacker)确实会在提供的补丁之间插入一些间隙。因此,即使不是全部错误,它们的连接也会变得笨拙。
在下面的脚本中(改编自 here),我尝试应用 matplotlib.offset 类为每个地轴(cartopy 的地轴)创建比例尺。请注意,AnchoredScaleBar(AnchoredOffsetbox 的子类)实现了整个 VPacker 和 HPacker 操作。在每个返回的地图中,都有一个比例尺(或至少是它的一部分)。
代码如下:
import cartopy.crs as ccrs
import cartopy.geodesic as cgeo
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle
from matplotlib.offsetbox import (AuxTransformBox, VPacker, HPacker, TextArea)
import matplotlib.transforms as transforms
from matplotlib.offsetbox import AnchoredOffsetbox
class AnchoredScaleBar(AnchoredOffsetbox):
def __init__(self, ax, transform, xcoords, height, xlabels=None,
ylabels=None, fontsize=4,
pad=0.1, borderpad=0.1, sep=2, prop=None, **kwargs):
"""
Draw a horizontal and/or vertical bar with the size in
data coordinate of the give axes. A label will be drawn
underneath (center-aligned).
- transform : the coordinate frame (typically axes.transData)
- sizex,sizey : width of x,y bar, in data units. 0 to omit
- labelx,labely : labels for x,y bars; None to omit
- pad, borderpad : padding, in fraction of the legend
font size (or prop)
- sep : separation between labels and bars in points.
- **kwargs : additional arguments passed to base class
constructor
"""
ATBs = []
for enum, xcor in enumerate(xcoords[1:]):
width = xcoords[1] - xcoords[0]
if enum % 2 == 0:
fc = 'white'
else:
fc = 'black'
Rect = Rectangle((0, 0), width, height, fc=fc,
edgecolor='k', zorder=99+enum)
ATB = AuxTransformBox(transform)
ATB.add_artist(Rect)
xlabel = xlabels[enum]
xlabel = int(xlabel)
Txt_xlabel = TextArea(xlabel,
textprops=dict(fontsize=fontsize),
minimumdescent=True)
# vertically packing a single stripe with respective label
child = VPacker(children=[Txt_xlabel,
ATB],
align="right", pad=5, sep=0)
# TODO: add legend to the child
# If we use ATBs.append(ATB), the resultant scalebar will have
# no ticks next to each strap
# If we use ATBs.append(child), there will be ticks. Though
# there will be spaces between each strap.
# While there is no solution for the problem, I am suggesting
# the first case scenario
# Therefore (TODO): add legend to the child
ATBs.append(child)
# horizontally packing all child packs in a single offsetBox
Children = HPacker(children=list(ATBs),
align="right", pad=0, sep=0)
Txt = TextArea('Km',
minimumdescent=False)
child = VPacker(children=[Children, Txt],
align="center", pad=2, sep=2)
AnchoredOffsetbox.__init__(self,
loc='center left',
borderpad=borderpad,
child=child,
prop=prop,
frameon=False,
**kwargs)
def _add_scalebar(ax, xcoords, height, xlabels=None,
ylabels=None,
fontsize=4,
bbox_to_anchor=(0.2, 0.5),
bbox_transform='axes fraction',
**kwargs):
""" Add scalebars to axes
Adds a set of scale bars to *ax*, matching the size
to the ticks of the plot
and optionally hiding the x and y axes
- ax : the axis to attach ticks to
- matchx,matchy : if True, set size of scale bars to spacing
between ticks
if False, size should be set using sizex and
sizey params
- hidex,hidey : if True, hide x-axis and y-axis of parent
- **kwargs : additional arguments passed to AnchoredScaleBars
Returns
created scalebar object
"""
blended_transform = transforms.blended_transform_factory(
ax.transData, ax.get_figure().dpi_scale_trans)
sb = AnchoredScaleBar(ax,
blended_transform,
xcoords,
height,
xlabels=xlabels,
ylabels=ylabels,
fontsize=fontsize,
bbox_transform=ax.transAxes,
bbox_to_anchor=bbox_to_anchor,
**kwargs)
sb.set_clip_on(False)
ax.add_artist(sb)
return sb
def get_unit_converter(unit):
lookuptable = {'km': 1000,
'mi': 1.60934 * 1000,
'dm': 1e-1,
'cm': 1e-2,
'mm': 1e-3,
'um': 1e-6,
'nm': 1e-9} # Miles to Km
return lookuptable.get(unit, 'km')
def _point_along_line(ax, start, distance, projected=False, verbose=False):
"""Point at a given distance from start at a given angle.
Args:
ax: CartoPy axes.
start: Starting point for the line in data coordinates.
distance: Positive physical distance to travel in meters.
angle: Anti-clockwise angle for the bar, in degrees. Default: 0
Returns:
(lon,lat) coords of a point (a (2, 1)-shaped NumPy array)
"""
# Direction vector of the line in axes coordinates.
if not projected:
geodesic = cgeo.Geodesic()
Direct_R = geodesic.direct(start, 90, distance)
target_longitude, target_latitude, forw_azi = Direct_R.base.T
target_point = ([target_longitude[0], target_latitude[0]])
actual_dist = geodesic.inverse(start,
target_point).base.ravel()[0]
if verbose:
print('Starting point', start)
print('target point', target_point)
print('Expected distance between points: ', distance)
print('Actual distance between points: ', actual_dist)
if projected:
longitude, latitude = start
target_longitude = longitude + distance
target_point = (target_longitude, latitude)
if verbose:
print('Axes is projected? ', projected)
print('Expected distance between points: ', distance)
print('Actual distance between points: ',
target_longitude - longitude)
return start, target_point
def fancy_scalebar(ax,
bbox_to_anchor,
length,
unit_name='km',
dy=5,
max_stripes=5,
ytick_label_margins=0.25,
fontsize=8,
font_weight='bold',
rotation=45,
zorder=999,
paddings={'xmin': 0.1,
'xmax': 0.1,
'ymin': 0.3,
'ymax': 0.8},
bbox_kwargs={'facecolor': 'w',
'edgecolor': 'k',
'alpha': 0.7},
numeric_scale_bar=True,
numeric_scale_bar_kwgs={'x_text_offset': 0,
'y_text_offset': -40,
'box_x_coord': 0.5,
'box_y_coord': 0.01},
verbose=False):
'''
Description
----------
This function draws a scalebar in the given geoaxes.
Parameters
----------
ax (geoaxes):
bbox_to_anchor (length 2 tuple):
It sets where the scalebar will be drawn
in axes fraction units.
length (float):
The distance in geodesic meters that will be used
for generating the scalebar.
unit_name (str):
Standard (km).
angle (int or float): in azimuth degrees.
The angle that will be used for evaluating the scalebar.
If 90 (degrees), the distance between each tick in the
scalebar will be evaluated in respect to the longitude
of the map.
If 0 (degrees), the ticks will be evaluated in accordance
to variation in the latitude of the map.
dy (int or float):
The hight of the scalebar in axes fraction.
max_stripes (int):
The number of stripes present in the scalebar.
ytick_label_margins (int or float):
The size of the margins for drawing the scalebar ticklabels.
fontsize (int or float):
The fontsize used for drawing the scalebar ticklabels.
font_weight (str):
the fontweight used for drawing the scalebar ticklabels.
rotation (int or float):
the rotation used for drawing the scalebar ticklabels.
zorder(int):
The zorder used for drawing the scalebar.
paddings (dict):
A dictionary defining the padding to draw a background box
around the scalebar.
Example of allowed arguments for padding:
{'xmin': 0.3,
'xmax': 0.3,
'ymin': 0.3,
'ymax': 0.3}
bbox_kwargs (dict):
A dictionary defining the background box
around the scalebar.
Example of allowed arguments for padding:
{'facecolor': 'w',
'edgecolor': 'k',
'alpha': 0.7}
numeric_scale_bar(bool):
whether or not to draw a number scalebar along side the
graphic scalebar. Notice that this option can drastically
vary in value, depending on the geoaxes projection used.
numeric_scale_bar_kwgs (dict):
A dictionary defining the numeric scale bar.
Example of allowed arguments:
{'x_text_offset': 0,
'y_text_offset': -40,
'box_x_coord': 0.5,
'box_y_coord': 0.01}
Returns
----------
None
'''
proj_units = ax.projection.proj4_params.get('units', 'degrees')
if proj_units.startswith('deg'):
projected = False
elif proj_units.startswith('m'):
projected = True
# getting the basic unit converter for labeling the xticks
unit_converter = get_unit_converter(unit_name)
if verbose:
print('Axes is projected? ', projected)
# Convert all units and types.
# Map central XY data coordinates
x0, x1, y0, y1 = ax.get_extent()
central_coord_map = np.mean([[x0, x1], [y0, y1]], axis=1).tolist()
# End-point of bar in lon/lat coords.
start, end = _point_along_line(ax,
central_coord_map,
length,
projected=projected,
verbose=verbose)
# choose exact X points as sensible grid ticks with Axis 'ticker' helper
xcoords = np.empty(max_stripes + 1)
xlabels = []
xcoords[0] = start[0]
ycoords = np.empty_like(xcoords)
for i in range(0, max_stripes):
startp, endp = _point_along_line(ax, central_coord_map,
length * (i + 1),
projected=projected)
xcoords[i + 1] = endp[0]
ycoords[i + 1] = end[1]
label = round(length * (i + 1) / unit_converter)
xlabels.append(label)
# Stacking data coordinates (the target ticks of the scalebar) in a list
scalebar = _add_scalebar(ax, xcoords, dy, xlabels=xlabels,
ylabels=None,
fontsize=fontsize,
bbox_to_anchor=bbox_to_anchor)
return scalebar, xlabels
if '__main__' == __name__:
def test_scalebar():
"""Test"""
fig, axes = plt.subplots(2, 2,
subplot_kw={'projection':
ccrs.Mercator()})
projections = [ccrs.Mercator(), ccrs.PlateCarree(),
ccrs.Mercator(), ccrs.PlateCarree()]
axes = axes.ravel()
scalebars = []
for enum, (proj, ax) in enumerate(zip(projections, axes)):
ax.projection = proj
ax.set_title(str(proj).split(' ')[0].split('.')[-1])
if enum>=2:
length = 200_000
else:
length = 2_000_000
scalebar, xlabels = fancy_scalebar(ax,
bbox_to_anchor=(0.2, 0.2),
length=length,
unit_name='km',
max_stripes=4,
fontsize=10,
dy=0.05)
scalebars.append(scalebar)
gl = ax.gridlines(draw_labels=True)
gl.top_labels = False
gl.right_labels = False
ax.stock_img()
ax.coastlines()
if enum>=2:
ax.set_extent([-70, -45, -15, 10])
plt.tight_layout()
return axes, scalebars, xlabels
axes, scalebars, xlabels = test_scalebar()
经过一些尝试后,我注意到如果第 86 行(其中声明为“ATBs.append(child)”)更改为“ATBs.append(ATB)”,则比例尺会正确放置而没有间隙。
>然而,如果这样做,比例尺将丢失每个相应补丁的所有刻度标签(黑白矩形)。
这是第二种情况的图:
感谢所有帮助。
真诚的