From 62214eff41493ebcf1b26a6f74d29f87f3bc41ed Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Tue, 28 Jul 2026 08:52:01 +0000 Subject: [PATCH] Cycle only over filled markers for default artist style `Line2D.markers` is the full marker registry, not a list of usable plot markers: it contains the integer tick and caret markers, as well as markers that draw nothing. Plotting 24 or more lines thus picked an integer marker, raising `AttributeError: 'int' object has no attribute 'lower'`, and 35 or more lines would have silently produced invisible artists. Fixes #590 Co-Authored-By: Claude Opus 5 --- src/plopp/backends/matplotlib/line.py | 6 ++---- src/plopp/backends/matplotlib/scatter.py | 6 ++---- src/plopp/backends/matplotlib/utils.py | 13 +++++++++++++ tests/backends/matplotlib/mpl_line_test.py | 9 +++++++++ tests/backends/matplotlib/mpl_plot_test.py | 6 ++++++ 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/plopp/backends/matplotlib/line.py b/src/plopp/backends/matplotlib/line.py index 4c0691b1f..2ee03182a 100644 --- a/src/plopp/backends/matplotlib/line.py +++ b/src/plopp/backends/matplotlib/line.py @@ -9,12 +9,11 @@ import scipp as sc from matplotlib.axes import Axes from matplotlib.dates import date2num -from matplotlib.lines import Line2D from ...graphics.bbox import BoundingBox from ..common import check_ndim, make_line_bbox, make_line_data from .canvas import Canvas -from .utils import parse_dicts_in_kwargs +from .utils import default_marker, parse_dicts_in_kwargs def _to_float(x): @@ -255,11 +254,10 @@ def __init__( 'color': f'C{artist_number}', 'zorder': 2, } - markers = list(Line2D.markers.keys()) default_plot_style = { 'linestyle': 'none', 'linewidth': 1.5, - 'marker': markers[(artist_number + 2) % len(markers)], + 'marker': default_marker(artist_number), 'color': f'C{artist_number}', 'zorder': 2, } diff --git a/src/plopp/backends/matplotlib/scatter.py b/src/plopp/backends/matplotlib/scatter.py index d2fe0919c..ee1aebcc7 100644 --- a/src/plopp/backends/matplotlib/scatter.py +++ b/src/plopp/backends/matplotlib/scatter.py @@ -6,14 +6,13 @@ import numpy as np import scipp as sc -from matplotlib.lines import Line2D from ...core.utils import merge_masks from ...graphics.bbox import BoundingBox, axis_bounds from ...graphics.colormapper import ColorMapper from ..common import check_ndim from .canvas import Canvas -from .utils import parse_dicts_in_kwargs +from .utils import default_marker, parse_dicts_in_kwargs class Scatter: @@ -79,9 +78,8 @@ def __init__( self._unit = self._data.unit self._id = uuid.uuid4().hex - markers = list(Line2D.markers.keys()) default_plot_style = { - 'marker': markers[(artist_number + 2) % len(markers)], + 'marker': default_marker(artist_number), } if not cbar: default_plot_style['color'] = f'C{artist_number}' diff --git a/src/plopp/backends/matplotlib/utils.py b/src/plopp/backends/matplotlib/utils.py index 1818a8783..6dbd6760b 100644 --- a/src/plopp/backends/matplotlib/utils.py +++ b/src/plopp/backends/matplotlib/utils.py @@ -6,6 +6,7 @@ import matplotlib as mpl import matplotlib.pyplot as plt +from matplotlib.lines import Line2D def fig_to_bytes(fig: plt.Figure, form: Literal['png', 'svg'] = 'png') -> bytes: @@ -72,6 +73,18 @@ def make_figure(*args, **kwargs) -> plt.Figure: return fig +def default_marker(artist_number: int) -> str: + """ + Return a marker from a cycle of markers, based on the artist number. + + Only filled markers are used, as ``Line2D.markers`` also contains integer markers + and markers that draw nothing (e.g. ``'none'`` and ``''``). + """ + markers = Line2D.filled_markers + # Start the cycle at 'o' instead of the barely visible '.'. + return markers[(artist_number + 1) % len(markers)] + + def make_legend(leg: bool | tuple[float, float] | str) -> dict: """ Create a dict of arguments to be used in the legend creation. diff --git a/tests/backends/matplotlib/mpl_line_test.py b/tests/backends/matplotlib/mpl_line_test.py index ca02627d0..a362b6f06 100644 --- a/tests/backends/matplotlib/mpl_line_test.py +++ b/tests/backends/matplotlib/mpl_line_test.py @@ -4,6 +4,7 @@ import numpy as np import pytest import scipp as sc +from matplotlib.markers import MarkerStyle from plopp.backends.matplotlib.canvas import Canvas from plopp.backends.matplotlib.line import Line @@ -181,6 +182,14 @@ def test_kwarg_marker(): assert line._line.get_marker() == '+' +def test_default_marker_is_visible_for_large_artist_number(): + da = data_array(ndim=1) + canvas = Canvas() + for artist_number in range(50): + line = Line(canvas=canvas, data=da, artist_number=artist_number) + assert MarkerStyle(line.marker).is_filled() + + @pytest.mark.parametrize("mode", ['band', 'bar', True]) def test_line_color_with_errorbars(mode): from matplotlib.colors import to_hex diff --git a/tests/backends/matplotlib/mpl_plot_test.py b/tests/backends/matplotlib/mpl_plot_test.py index 398740f6d..b753c1845 100644 --- a/tests/backends/matplotlib/mpl_plot_test.py +++ b/tests/backends/matplotlib/mpl_plot_test.py @@ -202,3 +202,9 @@ def test_aspect_ratio(): da = data_array(ndim=2) fig = da.plot(aspect='equal') assert fig.canvas.ax.get_aspect() == 1.0 + + +def test_plot_many_lines(): + da = data_array(ndim=1) + fig = pp.plot({str(i): da for i in range(50)}) + assert len(fig.artists) == 50