Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Bug]: path snapping disabled for fully horizontal/vertical paths if unconnected path component is not fully vertical/horizontal #25263

Copy link
Copy link
Open
@anntzer

Description

@anntzer
Issue body actions

Bug summary

If a Path has multiple connected components (separated by MOVETO codes some of them fully horizontal/vertical and some of them not, then none of the chunks are snapped (this is controlled by PathSnapper::should_snap in path_converters.h); this means that drawing that Path via a single PathPatch (or a single Path in a Collection) will render slightly differently than if creating multiple Paths, one per connected component, and drawing them via multiple PathPatches (or a single Collection with multiple Paths).

Code for reproduction

from io import BytesIO
import matplotlib as mpl
from matplotlib import pyplot as plt
from matplotlib.patches import PathPatch
from matplotlib.path import Path
import numpy as np

mpl.use("qtagg")
mpl.rcParams["path.snap"] = True  # Default, set to False to remove the problem.

fig = plt.figure(figsize=(6, 2), dpi=200)
ax = fig.add_subplot()
# Define a path with three connected components; the last one has slanted parts.
xys = [
    [0.0, 4.5],
    [0.740033943422379, 4.5],
    [1.259966056577621, 4.5],
    [1.774087425970262, 4.5],
    [2.2259125740297376, 4.5],
    [3.0, 4.5],
    [4.0, 4.5],
    [4.5, 4.0],
    [4.5, 3.0],
    [4.5, 2.0],
    [4.5, 1.0],
    [4.5, 0.0],
]
M = Path.MOVETO; L = Path.LINETO
codes = [M, L, M, L, M, L, L, L, L, L, L, L]

# Draw as single Path, save as png.
ax.clear(); ax.set(xlim=(0, 9), ylim=(0, 9)); ax.set_axis_off()
ax.add_artist(PathPatch(Path(xys, codes), facecolor="none"))
buf0 = BytesIO(); fig.savefig(buf0, format="png"); buf0.seek(0)

# Draw as multiple separate Paths, save as png.
ax.clear(); ax.set(xlim=(0, 9), ylim=(0, 9)); ax.set_axis_off()
for sl in [slice(0, 2), slice(2, 4), slice(4, 12)]:
    # One can also explicitly pass the codes (Path(xys[sl], codes[sl])); this makes no
    # difference with leaving the codes as None.
    ax.add_artist(PathPatch(Path(xys[sl]), facecolor="none"))
buf1 = BytesIO(); fig.savefig(buf1, format="png"); buf1.seek(0)

plt.close("all")

# Summarize the results.
im0 = plt.imread(buf0)[..., 0]  # monochrome, so we can just take one channel.
im1 = plt.imread(buf1)[..., 0]
axs = plt.figure(figsize=(8, 8), layout="constrained").subplots(3, sharex=True, sharey=True)
axs[0].imshow(im0, cmap="gray")
axs[1].imshow(im1, cmap="gray")
axs[2].imshow(im0 - im1, cmap="bwr", clim=(-1, 1))
axs[2].set(title=f"diff image; max={abs(im0 - im1).max():.4f}")

plt.show()

Actual outcome

test
Note that the difference in snapping results in the bottom diff image showing the two lines being slightly offset one from the other.

Expected outcome

First two images should be exactly identical, and the bottom diff should be zero.

Additional information

Even if fixing this is tricky (we probably need to get rid of should_snap (a global value for the entire path) and instead decide for each individual LINETO whether it should be snapped), I'm mostly opening this right now to document one of the reasons why #25247 (which indeed turns multiple separate LineCollections into a single Collection) causes quite a few small image differences.

Operating system

macOS

Matplotlib Version

3.7.0

Matplotlib Backend

qtagg

Python version

3.11

Jupyter version

ENOSUCHLIB

Installation

from source (.tar.gz)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.