如何在 matplotlib-cartopy geoaxes 中正确水平地打包不同的补丁而不会导致补丁之间出现间隙?

问题描述

最近,我一直在检查 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,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),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,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,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: ',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,'ymax': 0.3}

        bBox_kwargs (dict):
            A dictionary defining the background Box
            around the scalebar.

            Example of allowed arguments for padding:
                {'facecolor': 'w','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,'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,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,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,dy,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,0.2),length=length,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,xlabels = test_scalebar()

这是带有相应地图的结果图(每个地图都有给定的投影/扩展)。

注意比例尺补丁之间的间隙。

Maps with gaps within the respective scalebars


观察 1:

经过一些尝试后,我注意到如果第 86 行(其中声明为“ATBs.append(child)”)更改为“ATBs.append(ATB)”,则比例尺会正确放置而没有间隙。

>

然而,如果这样做,比例尺将丢失每个相应补丁的所有刻度标签(黑白矩形)。


这是第二种情况的图:

注意比例尺上方缺少刻度标签

Map with correct scalebars,though without proper ticklabeling

感谢所有帮助。

真诚的

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...