Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7ca934f

Browse files
authored
Merge pull request #12380 from dstansby/stem-speedup2
Stem speedup2
2 parents 09b2b0d + 2e98633 commit 7ca934f

File tree

7 files changed

+112
-20
lines changed

7 files changed

+112
-20
lines changed
Lines changed: 14 additions & 0 deletions
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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2625,7 +2625,7 @@ def broken_barh(self, xranges, yrange, **kwargs):
26252625

26262626
@_preprocess_data()
26272627
def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
2628-
label=None):
2628+
label=None, use_line_collection=False):
26292629
"""
26302630
Create a stem plot.
26312631
@@ -2684,6 +2684,13 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
26842684
label : str, optional, default: None
26852685
The label to use for the stems in legends.
26862686
2687+
use_line_collection : bool, optional, default: False
2688+
If ``True``, store and plot the stem lines as a
2689+
`~.collections.LineCollection` instead of individual lines. This
2690+
significantly increases performance, and will become the default
2691+
option in Matplotlib 3.3. If ``False``, defaults to the old
2692+
behavior of using a list of `.Line2D` objects.
2693+
26872694
26882695
Returns
26892696
-------
@@ -2715,6 +2722,9 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
27152722
x = y
27162723
y = np.asarray(args[0], dtype=float)
27172724
args = args[1:]
2725+
self._process_unit_info(xdata=x, ydata=y)
2726+
x = self.convert_xunits(x)
2727+
y = self.convert_yunits(y)
27182728

27192729
# defaults for formats
27202730
if linefmt is None:
@@ -2763,24 +2773,39 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
27632773
else:
27642774
basestyle, basemarker, basecolor = _process_plot_format(basefmt)
27652775

2776+
# New behaviour in 3.1 is to use a LineCollection for the stemlines
2777+
if use_line_collection:
2778+
stemlines = []
2779+
stemlines = [((xi, bottom), (xi, yi)) for xi, yi in zip(x, y)]
2780+
stemlines = mcoll.LineCollection(stemlines, linestyles=linestyle,
2781+
colors=linecolor,
2782+
label='_nolegend_')
2783+
self.add_collection(stemlines)
2784+
# Old behaviour is to plot each of the lines individually
2785+
else:
2786+
cbook._warn_external(
2787+
'In Matplotlib 3.3 individual lines on a stem plot will be '
2788+
'added as a LineCollection instead of individual lines. '
2789+
'This significantly improves the performance of a stem plot. '
2790+
'To remove this warning and switch to the new behaviour, '
2791+
'set the "use_line_collection" keyword argument to True.')
2792+
stemlines = []
2793+
for xi, yi in zip(x, y):
2794+
l, = self.plot([xi, xi], [bottom, yi],
2795+
color=linecolor, linestyle=linestyle,
2796+
marker=linemarker, label="_nolegend_")
2797+
stemlines.append(l)
2798+
27662799
markerline, = self.plot(x, y, color=markercolor, linestyle=markerstyle,
27672800
marker=markermarker, label="_nolegend_")
27682801

2769-
stemlines = []
2770-
for thisx, thisy in zip(x, y):
2771-
l, = self.plot([thisx, thisx], [bottom, thisy],
2772-
color=linecolor, linestyle=linestyle,
2773-
marker=linemarker, label="_nolegend_")
2774-
stemlines.append(l)
2775-
27762802
baseline, = self.plot([np.min(x), np.max(x)], [bottom, bottom],
27772803
color=basecolor, linestyle=basestyle,
27782804
marker=basemarker, label="_nolegend_")
27792805

27802806
stem_container = StemContainer((markerline, stemlines, baseline),
27812807
label=label)
27822808
self.add_container(stem_container)
2783-
27842809
return stem_container
27852810

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

lib/matplotlib/container.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,17 @@ class StemContainer(Container):
175175
176176
baseline : :class:`~matplotlib.lines.Line2D`
177177
The artist of the horizontal baseline.
178-
179178
"""
180-
181179
def __init__(self, markerline_stemlines_baseline, **kwargs):
180+
"""
181+
Parameters
182+
----------
183+
markerline_stemlines_baseline : tuple
184+
Tuple of ``(markerline, stemlines, baseline)``.
185+
``markerline`` contains the `LineCollection` of the markers,
186+
``stemlines`` is a `LineCollection` of the main lines,
187+
``baseline`` is the `Line2D` of the baseline.
188+
"""
182189
markerline, stemlines, baseline = markerline_stemlines_baseline
183190
self.markerline = markerline
184191
self.stemlines = stemlines

lib/matplotlib/legend_handler.py

Lines changed: 29 additions & 6 deletions
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,43 @@ 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+
# change the function used by update_prop() from the default
617+
# to one that handles LineCollection
618+
orig_update_func = self._update_prop_func
619+
self._update_prop_func = self._copy_collection_props
620+
621+
for line in leg_stemlines:
622+
self.update_prop(line, stemlines, legend)
623+
624+
else:
625+
for lm, m in zip(leg_stemlines, stemlines):
626+
self.update_prop(lm, m, legend)
627+
628+
if using_linecoll:
629+
self._update_prop_func = orig_update_func
614630

615631
leg_baseline = Line2D([np.min(xdata), np.max(xdata)],
616632
[bottom, bottom])
617-
618633
self.update_prop(leg_baseline, baseline, legend)
619634

620-
artists = [leg_markerline]
621-
artists.extend(leg_stemlines)
635+
artists = leg_stemlines
622636
artists.append(leg_baseline)
637+
artists.append(leg_markerline)
623638

624639
for artist in artists:
625640
artist.set_transform(trans)
626641

627642
return artists
628643

644+
def _copy_collection_props(self, legend_handle, orig_handle):
645+
"""
646+
Method to copy properties from a LineCollection (orig_handle) to a
647+
Line2D (legend_handle).
648+
"""
649+
legend_handle.set_color(orig_handle.get_color()[0])
650+
legend_handle.set_linestyle(orig_handle.get_linestyle()[0])
651+
629652

630653
class HandlerTuple(HandlerBase):
631654
"""

lib/matplotlib/pyplot.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2877,11 +2877,12 @@ def stackplot(
28772877
@docstring.copy(Axes.stem)
28782878
def stem(
28792879
*args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
2880-
label=None, data=None):
2880+
label=None, use_line_collection=False, data=None):
28812881
return gca().stem(
28822882
*args, linefmt=linefmt, markerfmt=markerfmt, basefmt=basefmt,
2883-
bottom=bottom, label=label, **({"data": data} if data is not
2884-
None else {}))
2883+
bottom=bottom, label=label,
2884+
use_line_collection=use_line_collection, **({"data": data} if
2885+
data is not None else {}))
28852886

28862887

28872888
# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
Loading

lib/matplotlib/tests/test_axes.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3143,6 +3143,28 @@ def test_hist_stacked_weighted():
31433143
ax.hist((d1, d2), weights=(w1, w2), histtype="stepfilled", stacked=True)
31443144

31453145

3146+
@pytest.mark.parametrize("use_line_collection", [True, False],
3147+
ids=['w/ line collection', 'w/o line collection'])
3148+
@image_comparison(baseline_images=['stem'], extensions=['png'], style='mpl20',
3149+
remove_text=True)
3150+
def test_stem(use_line_collection):
3151+
x = np.linspace(0.1, 2 * np.pi, 100)
3152+
args = (x, np.cos(x))
3153+
# Label is a single space to force a legend to be drawn, but to avoid any
3154+
# text being drawn
3155+
kwargs = dict(linefmt='C2-.', markerfmt='k+', basefmt='C1-.',
3156+
label=' ', use_line_collection=use_line_collection)
3157+
3158+
fig, ax = plt.subplots()
3159+
if use_line_collection:
3160+
ax.stem(*args, **kwargs)
3161+
else:
3162+
with pytest.warns(UserWarning):
3163+
ax.stem(*args, **kwargs)
3164+
3165+
ax.legend()
3166+
3167+
31463168
def test_stem_args():
31473169
fig = plt.figure()
31483170
ax = fig.add_subplot(1, 1, 1)

0 commit comments

Comments
 (0)