Collections attribute deprecation in version > 3.8

Hi everyone,

I have been using matplotlib for a few years now, and I am really happy with it, therefore I’d first like to thank everyone contributing to the module and to this forum.

I have a little question about the versions > 3.8. I am using the matplotlib.pyplot.contour and contourf functions, and I’d like to access each path individually, even if they are at the same level. To do so, I used to use the collections attribute and everything worked, but now there is this little message that worries me:

“MatplotlibDeprecationWarning: The collections attribute was deprecated in Matplotlib 3.8 and will be removed two minor releases later.”

Does anyone know what I could use instead ? I mean, my program is working atm, but I’d like it to work in a few months as well - and it definitely won’t if the collection attribute is removed and if I have no subtitute.

I have checked the documentation, but found nothing about a substitution. I may have missed it tho.

Thanks for reading that far,

Matu

Since the ContourSet now inherits directly from Collection, you can use the get_paths method. E.g.

cs = plt.contour(...)
paths = cs.get_paths()

I agree that this is not as obvious as it could be from the documentation.

1 Like

Hi rcomer,

Thank you very much for your immediate answer.
This works perfectly.

Cheers,

Matu

1 Like

@rcomer

Hello! I am having a similar problem here, but in my case, in the past I was doing:
for i, (level, path_collection) in enumerate(zip(ax.levels, ax.collections)):
Which allowed me to separate each path per level. It seems like ax.get_paths() will put them all together.
The second issue I am having is that in the past, I would get in return open paths, but now it seems I am getting closed paths. Is there any way I can get both open paths again and separation by levels?
Thanks very much for your help!

@jreniel there is now one path per level so you can do

cs = plt.contour([[0, 1], [1, 2]])
for level, path in zip(cs.levels, cs.get_paths()):
    ...etc...

Within each path, individual contour lines are separated by the MOVETO code. I’m afraid I don’t understand your question about open vs closed paths: the actual contour lines are the same and it is only the way they are organised that has changed.

Hi @rcomer, thank you for the clarification, that makes sense.
In the past, all the countours were completely “independent” from each other. In this new version, when one finishes it connects to the next, and I believe this has to do with the MOVETO (maybe). In the current version, it seems I have to explicitly divide them, whereas in the previous version they were already divided.
In any case, I found another thread where you posted the solution: use contourpy directly.
Thanks a lot for your help!

1 Like

Hi, @rcomer.

Could you please clarify, what is the proper way to change the following code (which was removing contours from plot):

            if isinstance(contour, ContourSet):
                for lineset in contour.collections:
                    lineset.remove()

The problem for me is that if I try to replace collection with get_paths, then remove method does not work on path.
If I try removing path via del (for example, as follows):

            for k, path in enumerate(contour.get_paths()):
                 del contour.get_paths()[k]

then the contour to be removed remains in the image.

Many thanks in advance

@hedgehog if you want to remove all the contours you can go ahead and use the remove method on the ContourSet itself

contour.remove()

@rcomer Thank you very much

1 Like

Hello,
Can you explain this more.

@rileybailey do you have a specific question or problem related to this change?

@rcomer
MatplotlibDeprecationWarning: The collections attribute was deprecated in Matplotlib 3.8 and will be removed in 3.10.
for contour in originfig.collections:
I don’t know how to modify this, could help me please

for contour in originfig.collections:
        contour.set_clip_path(clip)

@liangblack since the ContourSet is now itself a collection, you should be able to just do

originfig.set_clip_path(clip)

You can also pass your clip path within the original call to contour(f):
https://matplotlib.org/stable/users/prev_whats_new/whats_new_3.8.0.html#clipping-for-contour-plots

Thanks very much for your help! :smiling_face:

1 Like

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():

  1. 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.
  2. 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).
  3. Each (separate) Path object would then require further processing as desired.

the new contour():

  1. 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.
  2. 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).
  3. 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
1 Like

Thanks @JRP, that was incredibly helpful. It used to be so easy to extract the data, I wish this change wouldn’t have been so disruptive.