Skip to content

Commit 2098d1a

Browse files
committed
Improve step performance
Do deprecation better Fix linestyle
1 parent d1060a8 commit 2098d1a

File tree

6 files changed

+114
-16
lines changed

6 files changed

+114
-16
lines changed
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
`StemContainer` now stores `LineCollection`
2+
-------------------------------------------
3+
4+
`StemContainer` objects can now store a `LineCollection` object instead of a
5+
list of `Line2D` objects for stem lines plotted using `ax.stem`. This gives a
6+
very large performance boost to displaying and moving `ax.stem` plots.
7+
8+
This will become the default behaviour in Matplotlib 3.3. To use it now, the
9+
``use_line_collection`` keyword argument to ~`.axes.stem` can be set to
10+
``True``.
11+
12+
Individual line segments can be extracted from the `LineCollection` using
13+
`LineCollection.get_segements()`. See the `LineCollection` documentation for
14+
other methods to retrieve the collection properties.

lib/matplotlib/axes/_axes.py

+34-9
Original file line numberDiff line numberDiff line change
@@ -2480,7 +2480,7 @@ def broken_barh(self, xranges, yrange, **kwargs):
24802480

24812481
@_preprocess_data()
24822482
def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
2483-
label=None):
2483+
label=None, use_line_collection=False):
24842484
"""
24852485
Create a stem plot.
24862486
@@ -2539,6 +2539,12 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
25392539
label : str, optional, default: None
25402540
The label to use for the stems in legends.
25412541
2542+
use_line_collection : bool, optional, default: False
2543+
If ``True``, store and plot the stem lines as a
2544+
~`.collections.LineCollection` instead of individual lines. This
2545+
significantly increases performance, and will become the default
2546+
option in Matplotlib 3.3. If ``False``, defaults to old behaviour.
2547+
25422548
25432549
Returns
25442550
-------
@@ -2570,6 +2576,9 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
25702576
x = y
25712577
y = np.asarray(args[0], dtype=float)
25722578
args = args[1:]
2579+
self._process_unit_info(xdata=x, ydata=y)
2580+
x = self.convert_xunits(x)
2581+
y = self.convert_yunits(y)
25732582

25742583
# defaults for formats
25752584
if linefmt is None:
@@ -2618,24 +2627,40 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
26182627
else:
26192628
basestyle, basemarker, basecolor = _process_plot_format(basefmt)
26202629

2630+
# New behaviour in 3.1 is to use a LineCollection for the stemlines
2631+
if use_line_collection:
2632+
stemlines = []
2633+
for thisx, thisy in zip(x, y):
2634+
stemlines.append(((thisx, bottom), (thisx, thisy)))
2635+
stemlines = mcoll.LineCollection(stemlines, linestyles=linestyle,
2636+
colors=linecolor,
2637+
label='_nolegend_')
2638+
self.add_collection(stemlines)
2639+
# Old behaviour is to plot each of the lines individually
2640+
else:
2641+
warnings.warn(
2642+
'In Matplotlib 3.3 individual lines on a stem plot will be '
2643+
'added as a LineCollection instead of individual lines.\n'
2644+
'This significantly improves the performance of a stem plot.\n'
2645+
'To remove this warning and switch to the new behaviour, '
2646+
'set the "use_line_collection" keyword argument to True.')
2647+
stemlines = []
2648+
for thisx, thisy in zip(x, y):
2649+
l, = self.plot([thisx, thisx], [bottom, thisy],
2650+
color=linecolor, linestyle=linestyle,
2651+
marker=linemarker, label="_nolegend_")
2652+
stemlines.append(l)
2653+
26212654
markerline, = self.plot(x, y, color=markercolor, linestyle=markerstyle,
26222655
marker=markermarker, label="_nolegend_")
26232656

2624-
stemlines = []
2625-
for thisx, thisy in zip(x, y):
2626-
l, = self.plot([thisx, thisx], [bottom, thisy],
2627-
color=linecolor, linestyle=linestyle,
2628-
marker=linemarker, label="_nolegend_")
2629-
stemlines.append(l)
2630-
26312657
baseline, = self.plot([np.min(x), np.max(x)], [bottom, bottom],
26322658
color=basecolor, linestyle=basestyle,
26332659
marker=basemarker, label="_nolegend_")
26342660

26352661
stem_container = StemContainer((markerline, stemlines, baseline),
26362662
label=label)
26372663
self.add_container(stem_container)
2638-
26392664
return stem_container
26402665

26412666
@_preprocess_data(replace_names=["x", "explode", "labels", "colors"])

lib/matplotlib/container.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,17 @@ class StemContainer(Container):
174174
175175
baseline : :class:`~matplotlib.lines.Line2D`
176176
The artist of the horizontal baseline.
177-
178177
"""
179-
180178
def __init__(self, markerline_stemlines_baseline, **kwargs):
179+
'''
180+
Parameters
181+
----------
182+
markerline_stemlines_baseline : tuple
183+
Tuple of ``(markerline, stemlines, baseline)``.
184+
``markerline`` contains the `LineCollection` of the markers,
185+
``stemlines`` is a `LineCollection` of the main lines,
186+
``baseline`` is the `Line2D` of the baseline.
187+
'''
181188
markerline, stemlines, baseline = markerline_stemlines_baseline
182189
self.markerline = markerline
183190
self.stemlines = stemlines

lib/matplotlib/legend_handler.py

+35-5
Original file line numberDiff line numberDiff line change
@@ -589,8 +589,11 @@ def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize):
589589
def create_artists(self, legend, orig_handle,
590590
xdescent, ydescent, width, height, fontsize,
591591
trans):
592-
593592
markerline, stemlines, baseline = orig_handle
593+
# Check to see if the stemcontainer is storing lines as a list or a
594+
# LineCollection. Eventually using a list will be removed, and this
595+
# logic can also be removed.
596+
using_linecoll = isinstance(stemlines, mcoll.LineCollection)
594597

595598
xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
596599
width, height, fontsize)
@@ -609,23 +612,50 @@ def create_artists(self, legend, orig_handle,
609612
leg_stemlines = [Line2D([x, x], [bottom, y])
610613
for x, y in zip(xdata_marker, ydata)]
611614

612-
for lm, m in zip(leg_stemlines, stemlines):
613-
self.update_prop(lm, m, legend)
615+
if using_linecoll:
616+
# update_prop() usually takes two Line2D collections;
617+
# override temporarily to copy properties from a LineCollection
618+
orig_update_func = self._update_prop_func
619+
self._update_prop_func = self._copy_collection_props
620+
621+
for thisx, thisy in zip(xdata_marker, ydata):
622+
thisline = Line2D([thisx, thisx], [bottom, thisy])
623+
leg_stemlines.append(thisline)
624+
self.update_prop(thisline, stemlines, legend)
625+
626+
self._update_prop_func = orig_update_func
627+
else:
628+
for thisx, thisy in zip(xdata_marker, ydata):
629+
thisline = Line2D([thisx, thisx], [bottom, thisy])
630+
leg_stemlines.append(thisline)
631+
for lm, m in zip(leg_stemlines, stemlines):
632+
self.update_prop(lm, m, legend)
614633

615634
leg_baseline = Line2D([np.min(xdata), np.max(xdata)],
616635
[bottom, bottom])
617636

618637
self.update_prop(leg_baseline, baseline, legend)
619638

620-
artists = [leg_markerline]
621-
artists.extend(leg_stemlines)
639+
leg_markerline = Line2D(xdata_marker, ydata[:len(xdata_marker)])
640+
self.update_prop(leg_markerline, markerline, legend)
641+
642+
artists = leg_stemlines
622643
artists.append(leg_baseline)
644+
artists.append(leg_markerline)
623645

624646
for artist in artists:
625647
artist.set_transform(trans)
626648

627649
return artists
628650

651+
def _copy_collection_props(self, legend_handle, orig_handle):
652+
'''
653+
Method to copy properties from a LineCollection (orig_handle) to a
654+
Line2D (legend_handle).
655+
'''
656+
legend_handle.set_color(orig_handle.get_color()[0])
657+
legend_handle.set_linestyle(orig_handle.get_linestyle()[0])
658+
629659

630660
class HandlerTuple(HandlerBase):
631661
"""
Loading

lib/matplotlib/tests/test_axes.py

+22
Original file line numberDiff line numberDiff line change
@@ -3072,6 +3072,28 @@ def test_hist_stacked_weighted():
30723072
ax.hist((d1, d2), weights=(w1, w2), histtype="stepfilled", stacked=True)
30733073

30743074

3075+
@pytest.mark.parametrize("use_line_collection", [True, False],
3076+
ids=['w/ line collection', 'w/o line collection'])
3077+
@image_comparison(baseline_images=['stem'], extensions=['png'], style='mpl20',
3078+
remove_text=True)
3079+
def test_stem(use_line_collection):
3080+
x = np.linspace(0.1, 2 * np.pi, 100)
3081+
args = (x, np.cos(x))
3082+
# Label is a single space to force a legend to be drawn, but to avoid any
3083+
# text being drawn
3084+
kwargs = dict(linefmt='C2-.', markerfmt='k+', basefmt='C1-.',
3085+
label=' ', use_line_collection=use_line_collection)
3086+
3087+
fig, ax = plt.subplots()
3088+
if use_line_collection:
3089+
ax.stem(*args, **kwargs)
3090+
else:
3091+
with pytest.warns(UserWarning):
3092+
ax.stem(*args, **kwargs)
3093+
3094+
ax.legend()
3095+
3096+
30753097
def test_stem_args():
30763098
fig = plt.figure()
30773099
ax = fig.add_subplot(1, 1, 1)

0 commit comments

Comments
 (0)