Skip to content

[ENH]: Add long names for markers #29671

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
timhoffm opened this issue Feb 24, 2025 · 4 comments
Open

[ENH]: Add long names for markers #29671

timhoffm opened this issue Feb 24, 2025 · 4 comments

Comments

@timhoffm
Copy link
Member

Problem

Suggested in #29646 (comment)

In addition to the single-char (and in rare cases int) marker definitions, we should also support long names "o" -> "circle". These long names should be supported wherever the short mnemonics are, with the exception of the fmt string syntax in plot() - that should still only accept single chars.

This allows more readable code marker="tri_up" instead of marker="2". It also opens a route for deprecating the int markers, which create trouble for parsing/validating.

Proposed solution

The long names should mostly be the ones from the "description" column in https://matplotlib.org/devdocs/api/markers_api.html#module-matplotlib.markers, with some slight adjustments:

  • "filled_plus" instead of "plus (filled)"
  • do not include "nothing" - we already have more than enough no-marker definitions.
@CarterGamble
Copy link

Hi @timhoffm , I'm new to contributing to open source, but for this feature, couldn't we just invert the MarkerStyle dictionary and use the long names that already exist as keys?

@timhoffm
Copy link
Member Author

Almost. You can do

diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py
index 9c6b3da73b..bb5c09f99f 100644
--- a/lib/matplotlib/markers.py
+++ b/lib/matplotlib/markers.py
@@ -310,6 +310,8 @@ class MarkerStyle:
             self._marker_function = self._set_mathtext_path
         elif isinstance(marker, (int, str)) and marker in self.markers:
             self._marker_function = getattr(self, '_set_' + self.markers[marker])
+        elif marker != "nothing" and marker in self.markers.values():
+            self._marker_function = getattr(self, '_set_' + marker)
         elif (isinstance(marker, np.ndarray) and marker.ndim == 2 and
                 marker.shape[1] == 2):
             self._marker_function = self._set_vertices

which should basically do the trick. There are still some additional aspects to consider:

  • Do we want line.get_marker() to return the representation that was used to create it or a normalized one? If normalized, which one?
  • Add tests for the new marker spellings - I think we can reuse the image comparison from test_marker_styles.
  • Documentation needs update.
  • ... (and some more concerning the usage of that dict which I've already checked and they do not need to be adapted)

@CarterGamble
Copy link

  • Do we want line.get_marker() to return the representation that was used to create it or a normalized one? If normalized, which one?
    • In my opinion, using the representation used to create the marker would make the most sense to the user. Would this need to be tested?
  • Add tests for the new marker spellings - I think we can reuse the image comparison from test_marker_styles.
    • I couldn't find that test, but I did make a couple tests (one for scatter, one for line) like:
@check_figures_equal()
def test_long_name_scatter_plot(fig_ref, fig_test):
    ax_ref = fig_ref.add_axes([0, 0, 1, 1])
    ax_test = fig_test.add_axes([0, 0, 1, 1])
    i = 0
    for key, value in markers.MarkerStyle.markers.items():
        if key != ',':
            ax_ref.scatter([i], [1], s=50, marker=key)
            ax_test.scatter([i], [1], s=50, marker=value)
            i += 1 
  • Documentation needs update.
    • I'm not quite familiar with the documentation stuff, so some pointers would be nice, but I can probably figure it out. I was thinking of adding the long name markers to the "marker" column separated from the short name with the word "or". Ex: "." or "point"
  • ... (and some more concerning the usage of that dict which I've already checked and they do not need to be adapted)
    • So no updates to code other than what we already mentioned?

@timhoffm
Copy link
Member Author

  • Do we want line.get_marker() to return the representation that was used to create it or a normalized one? If normalized, which one?

    • In my opinion, using the representation used to create the marker would make the most sense to the user. Would this need to be tested?

There are arguments for both

# input-output equivalence - I get back what I've input
marker = 'o'
line, = plt.plot(x, y, marker=marker)
assert line.get_marker() == maker

# logical equivalence - both markers are semantically the same
line1, = plt.plot(x, y, marker='o')
line2, = plt.plot(x, y, marker='circle')
assert line1.get_marker() == line2.get_marker()

but we cannot have both. Unfortunately, the behavior is not even uniform throughout the library some aspects are normalized others are not. In another PR I argued for normalization due to simpler internal handling (but that's not an arguement here). I'm still on the +0 side for normalization, but can't make a strong case either way.

I recommend a test. This is public API and people will rely on it. We shouldn't accidentally break it.

  • Add tests for the new marker spellings - I think we can reuse the image comparison from test_marker_styles.

    • I couldn't find that test, but I did make a couple tests (one for scatter, one for line) like:

@check_figures_equal()
def test_long_name_scatter_plot(fig_ref, fig_test):
ax_ref = fig_ref.add_axes([0, 0, 1, 1])
ax_test = fig_test.add_axes([0, 0, 1, 1])
i = 0
for key, value in markers.MarkerStyle.markers.items():
if key != ',':
ax_ref.scatter([i], [1], s=50, marker=key)
ax_test.scatter([i], [1], s=50, marker=value)
i += 1

def test_marker_styles():

Your solution is ok as well.

  • Documentation needs update.

    • I'm not quite familiar with the documentation stuff, so some pointers would be nice, but I can probably figure it out. I was thinking of adding the long name markers to the "marker" column separated from the short name with the word "or". Ex: "." or "point"
  • Yes, the the table in https://matplotlib.org/stable/api/markers_api.html, "." or "point" is good.
  • https://matplotlib.org/stable/gallery/lines_bars_and_markers/marker_reference.html is half a duplication. The sections unfilled makers/filled markers should somehow contain the long names - decide whether also add it to the images (better because it's more visible but difficult to layout) or just add a comment that there are also long names and link to the other table.
  • The docstring of Line2D.get_marker should explain which form of the marker normalized/unnormalized is returned. Since it's becoming public API, people will rely on it anyway, and then it's better we're explicit about it.
  • Generally, MarkerStyle should get one additional sentence that we have long and short names for markers.
  • The Attributes documentation in the MarkerStyle docstring should be updated:
    • markers: Mapping short names to long names
    • filled_markers list of filled markers denoted by their short names.

This should mostly cover it.

  • ... (and some more concerning the usage of that dict which I've already checked and they do not need to be adapted)

    • So no updates to code other than what we already mentioned?
      I'm not aware of any additionally needed changes:
  • MarkerStyle.markers and (identically) Line2D.markers / matplotlib.line2d.lineMarkers can stay as is. They are still a full list of markers - now explicitly demarked through the short name.
  • The parser for plot(..., fmt) uses these. Still correct, fmt should only support the single char froms.
  • Am I missing something?

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

2 participants