自定义 Matplotlib 投影展示

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在本实验中,我们将学习如何使用 Matplotlib 创建自定义投影。我们将通过展示 Matplotlib 的许多特性来展示 Hammer 投影。我们将使用 Python 作为编程语言。

虚拟机提示

虚拟机启动完成后,点击左上角切换到“笔记本”标签,以访问 Jupyter Notebook 进行练习。

有时,你可能需要等待几秒钟让 Jupyter Notebook 完成加载。由于 Jupyter Notebook 的限制,操作验证无法自动化。

如果你在学习过程中遇到问题,请随时向 Labby 提问。课程结束后提供反馈,我们将立即为你解决问题。

导入库

首先,我们将导入创建自定义投影所需的库。

import numpy as np
import matplotlib
from matplotlib.axes import Axes
import matplotlib.axis as maxis
from matplotlib.patches import Circle
from matplotlib.path import Path
from matplotlib.projections import register_projection
import matplotlib.spines as mspines
from matplotlib.ticker import FixedLocator, Formatter, NullLocator
from matplotlib.transforms import Affine2D, BboxTransformTo, Transform

创建 GeoAxes 类

我们将创建一个名为 GeoAxes 的地理投影抽象基类。

class GeoAxes(Axes):
    """
    地理投影的抽象基类
    """

    class ThetaFormatter(Formatter):
        """
        用于格式化 theta 刻度标签。将弧度的原生单位转换为度并添加度符号。
        """
        def __init__(self, round_to=1.0):
            self._round_to = round_to

        def __call__(self, x, pos=None):
            degrees = round(np.rad2deg(x) / self._round_to) * self._round_to
            return f"{degrees:0.0f}\N{DEGREE SIGN}"

    RESOLUTION = 75

    def _init_axis(self):
        self.xaxis = maxis.XAxis(self)
        self.yaxis = maxis.YAxis(self)
        ## 在 GeoAxes.xaxis.clear() 正常工作之前,不要像在 Axes._init_axis() 中那样将 xaxis 或 yaxis 注册到脊柱(spines)上。
        ## self.spines['geo'].register_axis(self.yaxis)

    def clear(self):
        ## 继承的文档字符串
        super().clear()

        self.set_longitude_grid(30)
        self.set_latitude_grid(15)
        self.set_longitude_grid_ends(75)
        self.xaxis.set_minor_locator(NullLocator())
        self.yaxis.set_minor_locator(NullLocator())
        self.xaxis.set_ticks_position('none')
        self.yaxis.set_ticks_position('none')
        self.yaxis.set_tick_params(label1On=True)
        ## 为什么我们需要打开 y 轴刻度标签,而 x 轴刻度标签已经打开了呢?

        self.grid(rcParams['axes.grid'])

        Axes.set_xlim(self, -np.pi, np.pi)
        Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0)

创建 HammerAxes 类

我们将为艾托夫 - 哈默投影(一种等积地图投影)创建一个自定义类,名为 HammerAxes

class HammerAxes(GeoAxes):
    """
    艾托夫 - 哈默投影的自定义类,这是一种等积地图投影。

    https://en.wikipedia.org/wiki/Hammer_projection
    """

    ## 投影必须指定一个名称。用户将使用这个名称来选择投影,
    ## 例如 ``subplot(projection='custom_hammer')``。
    name = 'custom_hammer'

    class HammerTransform(Transform):
        """基础的哈默变换。"""
        input_dims = output_dims = 2

        def __init__(self, resolution):
            """
            创建一个新的哈默变换。分辨率是在每个输入线段之间进行插值的步数,以近似其在弯曲的哈默空间中的路径。
            """
            Transform.__init__(self)
            self._resolution = resolution

        def transform_non_affine(self, ll):
            longitude, latitude = ll.T

            ## 预先计算一些值
            half_long = longitude / 2
            cos_latitude = np.cos(latitude)
            sqrt2 = np.sqrt(2)

            alpha = np.sqrt(1 + cos_latitude * np.cos(half_long))
            x = (2 * sqrt2) * (cos_latitude * np.sin(half_long)) / alpha
            y = (sqrt2 * np.sin(latitude)) / alpha
            return np.column_stack([x, y])

        def transform_path_non_affine(self, path):
            ## vertices = path.vertices
            ipath = path.interpolated(self._resolution)
            return Path(self.transform(ipath.vertices), ipath.codes)

        def inverted(self):
            return HammerAxes.InvertedHammerTransform(self._resolution)

    class InvertedHammerTransform(Transform):
        input_dims = output_dims = 2

        def __init__(self, resolution):
            Transform.__init__(self)
            self._resolution = resolution

        def transform_non_affine(self, xy):
            x, y = xy.T
            z = np.sqrt(1 - (x / 4) ** 2 - (y / 2) ** 2)
            longitude = 2 * np.arctan((z * x) / (2 * (2 * z ** 2 - 1)))
            latitude = np.arcsin(y*z)
            return np.column_stack([longitude, latitude])

        def inverted(self):
            return HammerAxes.HammerTransform(self._resolution)

    def __init__(self, *args, **kwargs):
        self._longitude_cap = np.pi / 2.0
        super().__init__(*args, **kwargs)
        self.set_aspect(0.5, adjustable='box', anchor='C')
        self.clear()

    def _get_core_transform(self, resolution):
        return self.HammerTransform(resolution)

注册投影

现在我们将向 Matplotlib 注册该投影,以便用户可以选择它。

register_projection(HammerAxes)

创建示例

最后,我们将使用自定义投影创建一个示例。

if __name__ == '__main__':
    import matplotlib.pyplot as plt

    ## 现在使用自定义投影创建一个简单示例。
    fig, ax = plt.subplots(subplot_kw={'projection': 'custom_hammer'})
    ax.plot([-1, 1, 1], [-1, -1, 1], "o-")
    ax.grid()

    plt.show()

总结

在本实验中,我们学习了如何使用 Matplotlib 创建自定义投影。通过利用 Matplotlib 的许多特性,我们创建了一个名为哈默投影(Hammer projection)的自定义投影。我们学习了如何创建 GeoAxes 类、HammerAxes 类,注册投影,并使用自定义投影创建一个示例。