Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
anntzer opened this issue Feb 19, 2023 · 0 comments

Comments

@anntzer
Copy link
Contributor

anntzer commented Feb 19, 2023

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant