Skip to content

Fix half-filled markers protrusion #19354

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

Closed
wants to merge 1 commit into from
Closed

Conversation

anntzer
Copy link
Contributor

@anntzer anntzer commented Jan 24, 2021

Don't let the miter joins protrude by manually cropping them (#14357). This is
currently only implemented for diamond, but others should work
"similarly". However, this reveals a bug(?) in Agg's handling of
miter with close corners, which does not handle that case correctly.
(pdf, ps, svg, and cairo all handle it fine).

Also don't forget a closepoly on half-filled squares.

Goes on top of #19357.

PR Summary

PR Checklist

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (run flake8 on changed files to check).
  • New features are documented, with examples if plot related.
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • Conforms to Matplotlib style conventions (install flake8-docstrings and run flake8 --docstring-convention=all).
  • New features have an entry in doc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented in doc/api/next_api_changes/ (follow instructions in README.rst there).

@anntzer anntzer marked this pull request as draft January 24, 2021 20:22
@anntzer anntzer force-pushed the markers branch 2 times, most recently from 7fb8f86 to 91becf3 Compare January 25, 2021 07:01
@anntzer anntzer changed the title Shorten/make more consistent the half-filled marker definitions. Fix half-filled markers protrusion Jan 25, 2021
Don't let the miter joins protrude by manually cropping them.  This is
currently only implemented for diamond, but others should work
"similarly".  However, this reveals a bug(?) in Agg's handling of
miter with close corners, which does not handle that case correctly.
(pdf, ps, svg, and cairo all handle it fine).

Also don't forget a closepoly on half-filled squares.
@anntzer
Copy link
Contributor Author

anntzer commented Jan 11, 2023

I think the following is perhaps a better take on this: we "just" separately define and draw the surrounding path, instead of trying to use the paths that define the two halves of the marker, i.e.

diff --git i/lib/matplotlib/lines.py w/lib/matplotlib/lines.py
index f843ccd49f..68642cc100 100644
--- i/lib/matplotlib/lines.py
+++ w/lib/matplotlib/lines.py
@@ -817,7 +817,6 @@ class Line2D(Artist):
                     and not cbook._str_lower_equal(
                         self.get_markerfacecolor(), "none")):
                 ec_rgba = ec_rgba[:3] + (fc_rgba[3],)
-            gc.set_foreground(ec_rgba, isRGBA=True)
             if self.get_sketch_params() is not None:
                 scale, length, randomness = self.get_sketch_params()
                 gc.set_sketch_params(scale/2, length/2, 2*randomness)
@@ -854,24 +853,36 @@ class Line2D(Artist):
                 gc.set_capstyle(marker.get_capstyle())
                 marker_path = marker.get_path()
                 marker_trans = marker.get_transform()
+                alt_marker_path = marker.get_alt_path()
+                joint_surround_path = marker._get_joint_surround_path()
                 w = renderer.points_to_pixels(self._markersize)
 
-                if cbook._str_equal(marker.get_marker(), ","):
-                    gc.set_linewidth(0)
+                if not alt_marker_path:
+                    gc.set_foreground(ec_rgba, isRGBA=True)
+                    if cbook._str_equal(marker.get_marker(), ","):
+                        # Don't scale for pixels, and don't stroke them
+                        gc.set_linewidth(0)
+                    else:
+                        marker_trans = marker_trans.scale(w)
+                    renderer.draw_markers(
+                        gc, marker_path, marker_trans, subsampled,
+                        affine, fc_rgba)
+
                 else:
-                    # Don't scale for pixels, and don't stroke them
+                    gc.set_foreground((0, 0, 0, 0), isRGBA=True)
                     marker_trans = marker_trans.scale(w)
-                renderer.draw_markers(gc, marker_path, marker_trans,
-                                      subsampled, affine.frozen(),
-                                      fc_rgba)
-
-                alt_marker_path = marker.get_alt_path()
-                if alt_marker_path:
-                    alt_marker_trans = marker.get_alt_transform()
-                    alt_marker_trans = alt_marker_trans.scale(w)
                     renderer.draw_markers(
-                            gc, alt_marker_path, alt_marker_trans, subsampled,
-                            affine.frozen(), fcalt_rgba)
+                        gc, marker_path, marker_trans, subsampled,
+                        affine, fc_rgba)
+                    alt_marker_trans = marker.get_alt_transform().scale(w)
+                    renderer.draw_markers(
+                        gc, alt_marker_path, alt_marker_trans, subsampled,
+                        affine, fcalt_rgba)
+                    if joint_surround_path is not None:
+                        gc.set_foreground(ec_rgba, isRGBA=True)
+                        renderer.draw_markers(
+                            gc, joint_surround_path, marker_trans, subsampled,
+                            affine, (0, 0, 0, 0))
 
             gc.restore()
 
diff --git i/lib/matplotlib/markers.py w/lib/matplotlib/markers.py
index c9fc014193..0636a940a9 100644
--- i/lib/matplotlib/markers.py
+++ w/lib/matplotlib/markers.py
@@ -279,6 +279,7 @@ class MarkerStyle:
             return
         self._path = _empty_path
         self._transform = IdentityTransform()
+        self._joint_surround_path = None
         self._alt_path = None
         self._alt_transform = None
         self._snap_threshold = None
@@ -385,6 +386,9 @@ class MarkerStyle:
         else:
             return (self._transform + self._user_transform).frozen()
 
+    def _get_joint_surround_path(self):
+        return self._joint_surround_path
+
     def get_alt_path(self):
         """
         Return a `.Path` for the alternate part of the marker.
@@ -550,6 +554,12 @@ class MarkerStyle:
             self._path = Path.unit_circle()
         else:
             self._path = self._alt_path = Path.unit_circle_righthalf()
+            p = Path.unit_circle()
+            v = p.vertices.copy()
+            c = p.codes.copy()
+            v[-1] = (0, 1)
+            c[-1] = Path.LINETO
+            self._joint_surround_path = Path(v, c)
             fs = self.get_fillstyle()
             self._transform.rotate_deg(
                 {'right': 0, 'top': 90, 'left': 180, 'bottom': 270}[fs])
@@ -574,11 +584,15 @@ class MarkerStyle:
 
     _triangle_path = Path._create_closed([[0, 1], [-1, -1], [1, -1]])
     # Going down halfway looks to small.  Golden ratio is too far.
-    _triangle_path_u = Path._create_closed([[0, 1], [-3/5, -1/5], [3/5, -1/5]])
-    _triangle_path_d = Path._create_closed(
+    _triangle_path_t = Path._create_closed([[0, 1], [-3/5, -1/5], [3/5, -1/5]])
+    _triangle_path_b = Path._create_closed(
         [[-3/5, -1/5], [3/5, -1/5], [1, -1], [-1, -1]])
+    _triangle_path_tb = Path(
+        [[3/5, -1/5], [0, 1], [-3/5, -1/5], [3/5, -1/5], [1, -1], [-1, -1], [-3/5, -1/5]])
     _triangle_path_l = Path._create_closed([[0, 1], [0, -1], [-1, -1]])
     _triangle_path_r = Path._create_closed([[0, 1], [0, -1], [1, -1]])
+    _triangle_path_lr = Path(
+        [[0, 1], [0, -1], [-1, -1], [0, 1], [1, -1], [0, -1]])
 
     def _set_triangle(self, rot, skip):
         self._transform = Affine2D().scale(0.5).rotate_deg(rot)
@@ -587,24 +601,30 @@ class MarkerStyle:
         if not self._half_fill():
             self._path = self._triangle_path
         else:
-            mpaths = [self._triangle_path_u,
+            mpaths = [self._triangle_path_t,
                       self._triangle_path_l,
-                      self._triangle_path_d,
+                      self._triangle_path_b,
                       self._triangle_path_r]
+            jpaths = [self._triangle_path_tb,
+                      self._triangle_path_lr]
 
             fs = self.get_fillstyle()
             if fs == 'top':
                 self._path = mpaths[(0 + skip) % 4]
                 self._alt_path = mpaths[(2 + skip) % 4]
+                self._joint_surround_path = jpaths[skip % 2]
             elif fs == 'bottom':
                 self._path = mpaths[(2 + skip) % 4]
                 self._alt_path = mpaths[(0 + skip) % 4]
+                self._joint_surround_path = jpaths[skip % 2]
             elif fs == 'left':
                 self._path = mpaths[(1 + skip) % 4]
                 self._alt_path = mpaths[(3 + skip) % 4]
+                self._joint_surround_path = jpaths[(skip + 1) % 2]
             else:
                 self._path = mpaths[(3 + skip) % 4]
                 self._alt_path = mpaths[(1 + skip) % 4]
+                self._joint_surround_path = jpaths[(skip + 1) % 2]
 
             self._alt_transform = self._transform
 
@@ -648,6 +668,9 @@ class MarkerStyle:
         else:
             self._path = Path([[0, 0], [1, 0], [1, 1], [0, 0]])
             self._alt_path = Path([[0, 0], [0, 1], [1, 1], [0, 0]])
+            self._joint_surround_path = Path(
+                [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0], [1, 1]],
+                [Path.MOVETO, *[Path.LINETO] * 3, Path.CLOSEPOLY, Path.LINETO])
             fs = self.get_fillstyle()
             rotate = {'right': 0, 'top': 90, 'left': 180, 'bottom': 270}[fs]
             self._transform.rotate_deg(rotate)

Here the surrounding path is called _joint_surround_path, probably a better name is possible (and it could possibly be made public). The patch above only defines the surround paths for triangle and diamond paths to show that this approach can indeed fix #14357, but a full patch would need to also define the surround paths for the remaining markers (and then joint_surround_path would be guaranteed to always be non-None whenever alt_path is defined, so we'd get rid of the none-check on joint_surround_path in Line2D.draw).
Note that with this approach the midline is no longer stroked twice (which matters if mec is transparent, as in the example of #14357), but I'd say this actually looks better.

@github-actions
Copy link

Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it.

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Sep 29, 2023
@anntzer
Copy link
Contributor Author

anntzer commented Sep 29, 2023

Probably worth reviving in some form, but the basic issue is already tracked at #14357 so let's just close this for now, we can always reopen if anyone is interesting in picking up the patches.

@anntzer anntzer closed this Sep 29, 2023
@anntzer anntzer deleted the markers branch September 29, 2023 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: inactive Marked by the “Stale” Github Action status: needs rebase
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant