GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Behavior of `contains()` inconsistent with fill in overlapping paths · Issue #32329 · matplotlib/matplotlib

Behavior of contains() inconsistent with fill in overlapping paths #32329

New issue
Copy link
New issue
Copy link
Open
Open
Behavior of contains() inconsistent with fill in overlapping paths#32329
Copy link
Labels
topic: path handling

Description

@greglucas
greglucas
opened on Sep 10, 2026
Issue body actions

How do the two cases behave with contains() and mouse checking on the various artists?

I've investigated this, and amusingly enough, contains() is not fully consistent with either fill rule. The current logic (in this function) is essentially (and possibly unintentionally) consistent with the even-odd rule on any individual subpath (and if the subpath does not self-intersect, the non-zero winding rule is the same). However, if there are multiple subpaths, a point is considered "inside" the combined path if it is inside at least one subpath, even if the combined effect of all subpaths mean that the point should actually be "outside". This means:

  • If all subpaths do not self-intersect and do not intersect with each other, contains() is consistent with both the non-zero winding rule and the even-odd rule (that is, both rules have the same result)
  • If there is a single subpath that self-intersects, contains() is consistent with the even-odd rule
  • If there are multiple subpaths that do not self-intersect but do intersect with each other, and all subpaths are the same orientation, contains() is consistent with the non-zero winding rule
  • If none of the above, contains() gives a "wrong" result: it is not consistent with either rule

My inclination is that fixing contains() should be deferred to a future PR.

Originally posted by @ayshih in #32253 (comment)

Activity

  1. iccir
    added
    topic: path handling
    on Sep 10, 2026
  2. ayshih commented on Sep 10, 2026

    @ayshih
    ayshih
    on Sep 10, 2026
    Contributor
    More actions

    Here is an interactive example that shows how the existing contains() code sometimes matches one fill rule and not the other, depending on the path used for the PathPatch.

    import numpy as np
    
    import matplotlib.pyplot as plt
    from matplotlib.patches import PathPatch
    from matplotlib.path import Path
    
    twisted_vertices = np.array([[0, 0, 6, 6, 1, 1, 5, 5, 2, 2, 4, 4, 3, 3, 5, 5, 0],
                                 [2, 5, 5, 0, 0, 7, 7, 3, 3, 4, 4, 6, 6, 1, 1, 2, 2]]).T
    twisted_codes = [Path.MOVETO] + [Path.LINETO] * (len(twisted_vertices) - 1)
    
    square_vertices = np.array([[-1, -1], [-1, 1], [1, 1], [1, -1], [-1, -1]])
    square_codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO]
    
    fig, axs = plt.subplots(2, 3, figsize=(7, 4), layout='constrained')
    
    for i, fill_rule in enumerate(['nonzero', 'evenodd']):
        axs[i, 0].text(-1.2, 0, f'fill_rule={fill_rule}', rotation='vertical', va='center')
    
        donut1 = Path(np.vstack([square_vertices, square_vertices / 2]), square_codes * 2)
        axs[i, 0].add_patch(PathPatch(donut1, fc='g', ec='m', fill_rule=fill_rule))
    
        twisted = Path(twisted_vertices, twisted_codes)
        axs[i, 1].add_patch(PathPatch(twisted, fc='g', ec='m', fill_rule=fill_rule))
    
        donut2 = Path(np.vstack([square_vertices, square_vertices[::-1, :] / 2]), square_codes * 2)
        axs[i, 2].add_patch(PathPatch(donut2, fc='g', ec='m', fill_rule=fill_rule))
    
        for j in range(axs.shape[1]):
            axs[i, j].autoscale_view()
            axs[i, j].set_axis_off()
    
    axs[0, 0].set_title('contains() == nonzero')
    axs[0, 1].set_title('contains() == even-odd')
    axs[0, 2].set_title('contains() != either')
    
    def on_mouse_move(event):
        if event.inaxes is None:
            return
    
        for ax in axs.flatten():
            patch = ax.patches[0]
            patch.set_facecolor('r' if patch.contains(event)[0] else 'g')
    
        fig.canvas.draw_idle()
    
    fig.canvas.mpl_connect('motion_notify_event', on_mouse_move)
    
    plt.show()
  3. ayshih commented on Sep 10, 2026

    @ayshih
    ayshih
    on Sep 10, 2026
    Contributor
    More actions

    I've already verified how to modify point_in_path_impl() in _path.h to be fully consistent with one fill rule or the other. The thought needed is how to set up the (internal) API to carry the current fill rule down to that level to switch which one is being calculated.

    Reacted by Greg Lucas
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: path handling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.