As some people said the functionality of contour() has changed after collections got deprecated. However, it’s confusing how they have implemented the changes, especially to users that used multiple levels and wanted to manipulate individual paths at each level. Below I describe the differences for users that are manipulating Path objects with contour() and provide an example:
Starting off with you get a QuadContourSet from contour():
contour_set = contour(...)
the old contour():
- Get the contour_set.collections from it. This collections is a generator that you can iterate over to return the collection belonging to each level (layer), where the level (layer) is the collection.
- Then, for each collection you can get the paths for all contours (complete or not) by using get_paths() on the collection, which returns a list of Path objects (these are all the contours at the level).
- Each (separate) Path object would then require further processing as desired.
the new contour():
- contour_set.get_paths() now returns a list of joined/flattened Path objects, with each Path object representing the corresponding level (layer). This is a big change from the old version’s get_paths() – a more appropriate name would be get_levels_flattened_paths() for this function.
- Next, you should iterate over each (flattened) Path object (which represents all the contours at that level) using either iter_segments() or cleaned() as per the documentation (I use iter_segments() since I will do the processing myself).
- In the iter_segments() loop you get a (vertex_tuple, code) tuple (this is similar to Turtle Graphics), and you have to loop over each vertex and reconstruct each separate Path object using the code (I’ve only seen straight line segments from contour() so I only use 3 codes in the code below).
Here below is some code I wrote to be backwards compatible with both versions (as an example) that makes this clearer. For reference, this creates separate polygons and labels in a somewhat fancy way using straight lines based on relative vorticity data I process. (You probably could do without the Path import and just get the code enumeration values from the object itself.)
import matplotlib.pyplot as plt
from matplotlib.path import Path
def extract_contour_data(vorticity, levels, is_cyclonic=True, contour_id_counter=0):
contours = []
label_points = []
label_texts = []
contour_ids = []
span_lon_degs = []
span_lat_degs = []
# Generate contours
contour_set = plt.contour(vorticity.lon, vorticity.lat, vorticity, levels=levels,
colors='red' if is_cyclonic else 'blue')
# Check if get_paths() is not available (old version)
if not hasattr(contour_set, 'get_paths'):
# Old version: iterate over each layer
for i, collection in enumerate(contour_set.collections):
# get each path in the layer (level)
for path in collection.get_paths():
# Process path
contour_id_counter = process_path(path, levels[i], contours, label_points, label_texts, contour_ids,
span_lon_degs, span_lat_degs, contour_id_counter)
else:
# New version: iterate over get_paths() and generate paths from segments
# In new version get_paths() returns one joined path per level that has to be split up first to get each contour
paths_by_layer = []
for i, joined_paths_in_layer in enumerate(contour_set.get_paths()):
separated_paths_in_layer = []
path_vertices = []
path_codes = []
for verts, code in joined_paths_in_layer.iter_segments():
if code == Path.MOVETO:
if path_vertices:
separated_paths_in_layer.append(Path(np.array(path_vertices), np.array(path_codes)))
path_vertices = [verts]
path_codes = [code]
elif code == Path.LINETO:
path_vertices.append(verts)
path_codes.append(code)
elif code == Path.CLOSEPOLY:
path_vertices.append(verts)
path_codes.append(code)
if path_vertices:
separated_paths_in_layer.append(Path(np.array(path_vertices), np.array(path_codes)))
paths_by_layer.append(separated_paths_in_layer)
for i, paths_in_layer in enumerate(paths_by_layer):
# Process path
for path in paths_in_layer:
contour_id_counter = process_path(path, levels[i], contours, label_points, label_texts, contour_ids,
span_lon_degs, span_lat_degs, contour_id_counter)
return contours, label_points, label_texts, contour_ids, span_lon_degs, span_lat_degs, contour_id_counter
def process_path(path, level, contours, label_points, label_texts, contour_ids, span_lon_degs, span_lat_degs, contour_id_counter):
if not path.vertices.size:
return contour_id_counter
# Check the number of vertices
if len(path.vertices) < 4:
return contour_id_counter
try:
# Create polygon if valid
geom = Polygon(path.vertices)
if geom.is_valid:
contour_id_counter += 1
span_lon, span_lat = calculate_span(geom)
contours.append(geom)
span_lon_degs.append(span_lon)
span_lat_degs.append(span_lat)
contour_ids.append(contour_id_counter)
# Adding labels to the contour boundaries
cardinal_position = level % 4 # Determine the cardinal position based on level index
cardinal_point = get_cardinal_point(path.vertices, cardinal_position)
label_points.append(Point(cardinal_point))
label_texts.append(f"{level}")
except Exception as e:
print(f"Error creating Polygon: {e}")
return contour_id_counter