Repository: matplotlib/matplotlib

Issue:
`PathSimplifier` fails to ignore `CLOSEPOLY` vertices
<!--To help us understand and resolve your issue, please fill out the form to the best of your ability.-->
<!--You can feel free to delete the sections that do not apply.-->

### Bug report

**Bug summary**

This bug does not seem to have surfaced anywhere yet, but `Path`'s with `NaN` vertices at `CLOSEPOLY` can come through `PathNanRemover` in a "correct" state but then `PathSimplifier` can sometimes break it:

**Code for reproduction**/**Actual Outcome**

<!--A minimum code snippet required to reproduce the bug.
Please make sure to minimize the number of dependencies required, and provide
any necessary plotted data.
Avoid using threads, as Matplotlib is (explicitly) not thread-safe.-->


```python
>>> p = Path([(0, 0), (1, 0), (1, 1), (np.nan, np.nan)],
         [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
>>> # correctly ignores CLOSEPOLY even though it's NaN
>>> p.cleaned(remove_nans=True)
Path(array([[ 0.,  0.],
       [ 1.,  0.],
       [ 1.,  1.],
       [nan, nan],
       [ 0.,  0.]]), array([ 1,  2,  2, 79,  0], dtype=uint8))
>>> # but now for some reason these NaN's are used to populate LINETO's
>>> p.cleaned(remove_nans=True, simplify=True)
Path(array([[ 0.,  0.],
       [ 1.,  0.],
       [ 1.,  1.],
       [nan, nan],
       [nan, nan],
       [ 0.,  0.]]), array([1, 2, 2, 2, 2, 0], dtype=uint8))
```

**Expected outcome**

<!--A description of the expected outcome from the code snippet-->
<!--If this used to work in an earlier version of Matplotlib, please note the version it used to work on-->

The values of the vertices in a `CLOSEPOLY` should always be ignored, in favor of the most recent `MOVETO`'s vertex values:

```python
>>> p.cleaned(remove_nans=True, simplify=True)
Path(array([[ 0.,  0.],
       [ 1.,  0.],
       [ 1.,  1.],
       [0, 0],
       [ 0.,  0.]]), array([1, 2, 2, 2, 0], dtype=uint8))
```

**Matplotlib version**
<!--Please specify your platform and versions of the relevant libraries you are using:-->
  * Operating system:
  * Matplotlib version: 
  * Matplotlib backend (`print(matplotlib.get_backend())`):
  * Python version:
  * Jupyter version (if applicable):
  * Other libraries: 

<!--Please tell us how you installed matplotlib and python e.g., from source, pip, conda-->
<!--If you installed from conda, please specify which channel you used if not the default-->




Discussion:
This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear.  Thanks for your help!
I think the bug is in the path simplification dealing with `nan` points (sorry for the non-contiguous numbering, only copied the interesting lines 😉 ) :

```python
In [4]: p = Path([(0, 0), (1, 0), (1, 1), (np.nan, np.nan)],
   ...:          [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
In [9]: p.cleaned(simplify=True)
Out[9]:
Path(array([[ 0.,  0.],
       [ 1.,  0.],
       [ 1.,  1.],
       [nan, nan],
       [nan, nan],
       [ 0.,  0.]]), array([1, 2, 2, 2, 2, 0], dtype=uint8))
In [13]: p.cleaned(simplify=True).cleaned(simplify=True)
Out[13]:
Path(array([[ 0.,  0.],
       [ 1.,  0.],
       [ 1.,  1.],
       [nan, nan],
       [nan, nan],
       [nan, nan],
       [ 0.,  0.]]), array([1, 2, 2, 2, 2, 2, 0], dtype=uint8))

In [14]: p.cleaned(simplify=True).cleaned(simplify=True)
Out[14]:
Path(array([[ 0.,  0.],
       [ 1.,  0.],
       [ 1.,  1.],
       [nan, nan],
       [nan, nan],
       [nan, nan],
       [ 0.,  0.]]), array([1, 2, 2, 2, 2, 2, 0], dtype=uint8))

In [15]: p.cleaned(simplify=True).cleaned(simplify=True).cleaned(simplify=True)
Out[15]:
Path(array([[ 0.,  0.],
       [ 1.,  0.],
       [ 1.,  1.],
       [nan, nan],
       [nan, nan],
       [nan, nan],
       [nan, nan],
       [ 0.,  0.]]), array([1, 2, 2, 2, 2, 2, 2, 0], dtype=uint8))
```

at it seems to insert an extra LINETO nan everytime it is run!

It is not surprising to me that the cleaning steps are not abealian so I am not sure I am too worried that

```python
In [17]: p.cleaned(simplify=True).cleaned(remove_nans=True)
Out[17]:
Path(array([[0., 0.],
       [1., 0.],
       [1., 1.],
       [0., 0.]]), array([1, 2, 2, 0], dtype=uint8))

In [18]: p.cleaned(remove_nans=True).cleaned(simplify=True)
Out[18]:
Path(array([[ 0.,  0.],
       [ 1.,  0.],
       [ 1.,  1.],
       [nan, nan],
       [nan, nan],
       [ 0.,  0.]]), array([1, 2, 2, 2, 2, 0], dtype=uint8))
```

are different and passing both keywords does the second: https://github.com/matplotlib/matplotlib/blob/f800bf61cd863a3d8b4d60268b924a4300ee1815/src/_path.h#L1033-L1045

The implemenation of the simplification is at https://github.com/matplotlib/matplotlib/blob/f800bf61cd863a3d8b4d60268b924a4300ee1815/src/path_converters.h#L694-L912

I suspect that the fix is to add a few lines of special-casing nan because nan is equal to nothing and all math involving nans results in nan so the "is the vector going the same direction enough" logic fails.

I'm marking this as a good first issue (there is no API design and a pretty clear tast case), but has medium difficulty.

To take this issue on you should know both Python and C++ pretty well (including templates, classes, and understanding the `vertex` iterator protocol).  Review on this is likely to be slow.
### Good first issue - notes for new contributors

This issue is suited to new contributors because it does not require understanding of the Matplotlib internals. To get started, please see our [contributing guide](https://matplotlib.org/stable/devel/index).

**We do not assign issues**. Check the *Development* section in the sidebar for linked pull requests (PRs). If there are none, feel free to start working on it. If there is an open PR, please collaborate on the work by reviewing it rather than duplicating it in a competing PR.

If something is unclear, please reach out on any of our [communication channels](https://matplotlib.org/stable/devel/contributing.html#get-connected).



Patch:
diff --git a/src/path_converters.h b/src/path_converters.h
index db732e126c3f..a78643d64ed5 100644
--- a/src/path_converters.h
+++ b/src/path_converters.h
@@ -644,6 +644,13 @@ class PathSimplifier : protected EmbeddedQueue<9>
           m_after_moveto(false),
           m_clipped(false),
 
+          // whether the most recent MOVETO vertex is valid
+          m_has_init(false),
+
+          // the most recent MOVETO vertex
+          m_initX(0.0),
+          m_initY(0.0),
+
           // the x, y values from last iteration
           m_lastx(0.0),
           m_lasty(0.0),
@@ -754,6 +761,15 @@ class PathSimplifier : protected EmbeddedQueue<9>
                     _push(x, y);
                 }
                 m_after_moveto = true;
+
+                if (std::isfinite(*x) && std::isfinite(*y)) {
+                    m_has_init = true;
+                    m_initX = *x;
+                    m_initY = *y;
+                } else {
+                    m_has_init = false;
+                }
+
                 m_lastx = *x;
                 m_lasty = *y;
                 m_moveto = false;
@@ -768,6 +784,19 @@ class PathSimplifier : protected EmbeddedQueue<9>
             }
             m_after_moveto = false;
 
+            if(agg::is_close(cmd)) {
+                if (m_has_init) {
+                    /* If we have a valid initial vertex, then
+                       replace the current vertex with the initial vertex */
+                    *x = m_initX;
+                    *y = m_initY;
+                } else {
+                    /* If we don't have a valid initial vertex, then
+                       we can't close the path, so we skip the vertex */
+                    continue;
+                }
+            }
+
             /* NOTE: We used to skip this very short segments, but if
                you have a lot of them cumulatively, you can miss
                maxima or minima in the data. */
@@ -919,6 +948,8 @@ class PathSimplifier : protected EmbeddedQueue<9>
     bool m_moveto;
     bool m_after_moveto;
     bool m_clipped;
+    bool m_has_init;
+    double m_initX, m_initY;
     double m_lastx, m_lasty;
 
     double m_origdx;


Test patch:
diff --git a/lib/matplotlib/tests/test_simplification.py b/lib/matplotlib/tests/test_simplification.py
index 0a5c215eff30..a052c24cb655 100644
--- a/lib/matplotlib/tests/test_simplification.py
+++ b/lib/matplotlib/tests/test_simplification.py
@@ -518,3 +518,54 @@ def test_clipping_full():
     simplified = list(p.iter_segments(clip=[0, 0, 100, 100]))
     assert ([(list(x), y) for x, y in simplified] ==
             [([50, 40], 1)])
+
+
+def test_simplify_closepoly():
+    # The values of the vertices in a CLOSEPOLY should always be ignored,
+    # in favor of the most recent MOVETO's vertex values
+    paths = [Path([(1, 1), (2, 1), (2, 2), (np.nan, np.nan)],
+                  [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]),
+             Path([(1, 1), (2, 1), (2, 2), (40, 50)],
+                  [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])]
+    expected_path = Path([(1, 1), (2, 1), (2, 2), (1, 1), (1, 1), (0, 0)],
+                         [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
+                          Path.LINETO, Path.STOP])
+
+    for path in paths:
+        simplified_path = path.cleaned(simplify=True)
+        assert_array_equal(expected_path.vertices, simplified_path.vertices)
+        assert_array_equal(expected_path.codes, simplified_path.codes)
+
+    # test that a compound path also works
+    path = Path([(1, 1), (2, 1), (2, 2), (np.nan, np.nan),
+                 (-1, 0), (-2, 0), (-2, 1), (np.nan, np.nan)],
+                [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY,
+                 Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
+    expected_path = Path([(1, 1), (2, 1), (2, 2), (1, 1),
+                          (-1, 0), (-2, 0), (-2, 1), (-1, 0), (-1, 0), (0, 0)],
+                         [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
+                          Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
+                          Path.LINETO, Path.STOP])
+
+    simplified_path = path.cleaned(simplify=True)
+    assert_array_equal(expected_path.vertices, simplified_path.vertices)
+    assert_array_equal(expected_path.codes, simplified_path.codes)
+
+    # test for a path with an invalid MOVETO
+    # CLOSEPOLY with an invalid MOVETO should be ignored
+    path = Path([(1, 0), (1, -1), (2, -1),
+                 (np.nan, np.nan), (-1, -1), (-2, 1), (-1, 1),
+                 (2, 2), (0, -1)],
+                [Path.MOVETO, Path.LINETO, Path.LINETO,
+                 Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
+                 Path.CLOSEPOLY, Path.LINETO])
+    expected_path = Path([(1, 0), (1, -1), (2, -1),
+                          (np.nan, np.nan), (-1, -1), (-2, 1), (-1, 1),
+                          (0, -1), (0, -1), (0, 0)],
+                         [Path.MOVETO, Path.LINETO, Path.LINETO,
+                          Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
+                          Path.LINETO, Path.LINETO, Path.STOP])
+
+    simplified_path = path.cleaned(simplify=True)
+    assert_array_equal(expected_path.vertices, simplified_path.vertices)
+    assert_array_equal(expected_path.codes, simplified_path.codes)