1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
|
# Copyright Crown and Cartopy Contributors
#
# This file is part of Cartopy and is released under the BSD 3-clause license.
# See LICENSE in the root of the repository for full licensing details.
from matplotlib.contour import QuadContourSet
import matplotlib.path as mpath
import numpy as np
from cartopy.mpl import _MPL_38
class GeoContourSet(QuadContourSet):
"""
A contourset designed to handle things like contour labels.
"""
# nb. No __init__ method here - most of the time a GeoContourSet will
# come from GeoAxes.contour[f]. These methods morph a ContourSet by
# fiddling with instance.__class__.
def clabel(self, *args, **kwargs):
if not _MPL_38:
# nb: contour labelling does not work very well for filled
# contours - it is recommended to only label line contours.
# This is especially true when inline=True.
# This wrapper exist because mpl does not properly transform
# paths. Instead it simply assumes one path represents one polygon
# (not necessarily the case), and it assumes that
# transform(path.verts) is equivalent to transform_path(path).
# Unfortunately there is no way to easily correct this error,
# so we are forced to pre-transform the ContourSet's paths from
# the source coordinate system to the axes' projection.
# The existing mpl code then has a much simpler job of handling
# pre-projected paths (which can now effectively be transformed
# naively).
for col in self.collections:
# Snaffle the collection's path list. We will change the
# list in-place (as the contour label code does in mpl).
paths = col.get_paths()
# Define the transform that will take us from collection
# coordinates through to axes projection coordinates.
data_t = self.axes.transData
col_to_data = col.get_transform() - data_t
# Now that we have the transform, project all of this
# collection's paths.
new_paths = [col_to_data.transform_path(path)
for path in paths]
new_paths = [path for path in new_paths
if path.vertices.size >= 1]
# The collection will now be referenced in axes projection
# coordinates.
col.set_transform(data_t)
# Clear the now incorrectly referenced paths.
del paths[:]
for path in new_paths:
if path.vertices.size == 0:
# Don't persist empty paths. Let's get rid of them.
continue
# Split the path if it has multiple MOVETO statements.
codes = np.array(
path.codes if path.codes is not None else [0])
moveto = codes == mpath.Path.MOVETO
if moveto.sum() <= 1:
# This is only one path, so add it to the collection.
paths.append(path)
else:
# The first MOVETO doesn't need cutting-out.
moveto[0] = False
split_locs = np.flatnonzero(moveto)
split_verts = np.split(path.vertices, split_locs)
split_codes = np.split(path.codes, split_locs)
for verts, codes in zip(split_verts, split_codes):
# Add this path to the collection's list of paths.
paths.append(mpath.Path(verts, codes))
else:
# Where contour paths exist at the edge of the globe, sometimes a
# complete path in data space will become multiple paths when
# transformed into axes or screen space. Matplotlib's contour
# labelling does not account for this so we need to give it the
# pre-transformed paths to work with.
# Define the transform that will take us from collection
# coordinates through to axes projection coordinates.
data_t = self.axes.transData
col_to_data = self.get_transform() - data_t
# Now that we have the transform, project all of this
# collection's paths.
paths = self.get_paths()
new_paths = [col_to_data.transform_path(path) for path in paths]
self.set_paths(new_paths)
# The collection will now be referenced in axes projection
# coordinates.
self.set_transform(data_t)
# Now that we have prepared the collection paths, call on
# through to the underlying implementation.
return super().clabel(*args, **kwargs)
|