From b30e93b43b595e31be226100d490c81964c87d1f Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Tue, 9 Oct 2018 00:03:19 -0500 Subject: [PATCH 01/12] Warn when "best" loc of legend is used with lots of data --- lib/matplotlib/legend.py | 5 +++++ lib/matplotlib/tests/test_legend.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index b9a566345612..03d26b04a373 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1108,6 +1108,11 @@ def _find_best_position(self, width, height, renderer, consider=None): assert self.isaxes verts, bboxes, lines, offsets = self._auto_legend_data() + if len(verts) + len(bboxes) + len(lines) + len(offsets) > 10000: + warnings.warn( + 'Creating legend with loc="best" can be slow with large' + ' amounts of data.' + ) bbox = Bbox.from_bounds(0, 0, width, height) if consider is None: diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 031d075a8fe8..cffb9571eb91 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -563,3 +563,18 @@ def test_alpha_handles(): lh.set_alpha(1.0) assert lh.get_facecolor()[:-1] == hh[1].get_facecolor()[:-1] assert lh.get_edgecolor()[:-1] == hh[1].get_edgecolor()[:-1] + + +def test_warn_big_data_best_loc(): + fig, ax = plt.subplots() + ax.plot(np.arange(10001), label='Is this big data?') + with pytest.warns(UserWarning) as records: + l = ax.legend(loc='best') + l.draw(fig.canvas.get_renderer()) + # The _find_best_position method of Legend is called twice, duplicating + # the warning message. + assert len(records) == 2 + for record in records: + assert str(record.message) == ( + 'Creating legend with loc="best" can be slow with large' + ' amounts of data.') From cf1d30faf6525c05147c38afc2317667060e3c91 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Tue, 9 Oct 2018 09:35:36 -0500 Subject: [PATCH 02/12] Increase size limit before warning A legend with a `np.random.random(500000)` array plotted was a real pain to interact with. --- lib/matplotlib/legend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 03d26b04a373..f8d8210d84fc 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1108,7 +1108,7 @@ def _find_best_position(self, width, height, renderer, consider=None): assert self.isaxes verts, bboxes, lines, offsets = self._auto_legend_data() - if len(verts) + len(bboxes) + len(lines) + len(offsets) > 10000: + if len(verts) + len(bboxes) + len(lines) + len(offsets) > 500000: warnings.warn( 'Creating legend with loc="best" can be slow with large' ' amounts of data.' From f7be89ecd29174750132a1184d48e043911e1ab1 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Tue, 9 Oct 2018 11:44:49 -0500 Subject: [PATCH 03/12] Update data size in test I updated the N required in code but not in test. --- lib/matplotlib/tests/test_legend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index cffb9571eb91..c9f062c4795c 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -567,7 +567,7 @@ def test_alpha_handles(): def test_warn_big_data_best_loc(): fig, ax = plt.subplots() - ax.plot(np.arange(10001), label='Is this big data?') + ax.plot(np.arange(500001), label='Is this big data?') with pytest.warns(UserWarning) as records: l = ax.legend(loc='best') l.draw(fig.canvas.get_renderer()) From b6e044bac6f75cb5f117f848679544350f4e11e9 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Wed, 14 Nov 2018 18:22:59 -0600 Subject: [PATCH 04/12] Only warn if best was used by default --- lib/matplotlib/legend.py | 14 +++++++++++--- lib/matplotlib/tests/test_legend.py | 9 ++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index f8d8210d84fc..4da8609f67cf 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -485,9 +485,12 @@ def __init__(self, parent, handles, labels, self.parent = parent if loc is None: + self._loc_used_default = True loc = rcParams["legend.loc"] if not self.isaxes and loc in [0, 'best']: loc = 'upper right' + else: + self._loc_used_default = False if isinstance(loc, str): if loc not in self.codes: if self.isaxes: @@ -568,7 +571,8 @@ def __init__(self, parent, handles, labels, else: self.get_frame().set_alpha(framealpha) - self._loc = loc + self._set_loc(loc, is_initial_setting=True) + # figure out title fontsize: if title_fontsize is None: title_fontsize = rcParams['legend.title_fontsize'] @@ -588,10 +592,13 @@ def _set_artist_props(self, a): a.set_transform(self.get_transform()) - def _set_loc(self, loc): + def _set_loc(self, loc, is_initial_setting=False): # find_offset function will be provided to _legend_box and # _legend_box will draw itself at the location of the return # value of the find_offset. + if not is_initial_setting: + # User manually changed self._loc + self._loc_used_default = False self._loc_real = loc self.stale = True self._legend_box.set_offset(self._findoffset) @@ -1108,7 +1115,8 @@ def _find_best_position(self, width, height, renderer, consider=None): assert self.isaxes verts, bboxes, lines, offsets = self._auto_legend_data() - if len(verts) + len(bboxes) + len(lines) + len(offsets) > 500000: + if self._loc_used_default and verts.shape[0] > 200000: + # this size results in a 3+ second render time on a good machine warnings.warn( 'Creating legend with loc="best" can be slow with large' ' amounts of data.' diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index c9f062c4795c..4121a07232ba 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -567,10 +567,13 @@ def test_alpha_handles(): def test_warn_big_data_best_loc(): fig, ax = plt.subplots() - ax.plot(np.arange(500001), label='Is this big data?') + ax.plot(np.arange(200001), label='Is this big data?') with pytest.warns(UserWarning) as records: - l = ax.legend(loc='best') - l.draw(fig.canvas.get_renderer()) + l = ax.legend() + # We need to call an internal set method tricking it into thinking + # 'best' location was default. + l._set_loc(l.codes['best'], is_initial_setting=True) + fig.canvas.draw() # The _find_best_position method of Legend is called twice, duplicating # the warning message. assert len(records) == 2 From 6bce43f089c8559af80c6c2168d4efd0c1659567 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Wed, 14 Nov 2018 18:43:26 -0600 Subject: [PATCH 05/12] Add test that no warning is issued when best specified manually --- lib/matplotlib/tests/test_legend.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 4121a07232ba..dee45c56afc8 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -581,3 +581,14 @@ def test_warn_big_data_best_loc(): assert str(record.message) == ( 'Creating legend with loc="best" can be slow with large' ' amounts of data.') + + +def test_no_warn_big_data_when_loc_specified(): + fig, ax = plt.subplots() + ax.plot(np.arange(200001), label='Is this big data?') + with pytest.warns(None) as records: + l = ax.legend('best') + fig.canvas.draw() + # The _find_best_position method of Legend is called twice, duplicating + # the warning message. + assert len(records) == 0 From 504f61916d0714f10587ee207a4be62bd366d3cf Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Wed, 14 Nov 2018 18:51:03 -0600 Subject: [PATCH 06/12] update to new way of warning after rebase --- lib/matplotlib/legend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 4da8609f67cf..52924da825cd 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1117,7 +1117,7 @@ def _find_best_position(self, width, height, renderer, consider=None): verts, bboxes, lines, offsets = self._auto_legend_data() if self._loc_used_default and verts.shape[0] > 200000: # this size results in a 3+ second render time on a good machine - warnings.warn( + cbook._warn_external( 'Creating legend with loc="best" can be slow with large' ' amounts of data.' ) From 7997f4d034fec004d192392b44883c24bff2915f Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Wed, 14 Nov 2018 23:11:41 -0600 Subject: [PATCH 07/12] Remove unnecessarily copied comment in test copy/pasted but didn't get rid of non-applicable comment --- lib/matplotlib/tests/test_legend.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index dee45c56afc8..d17c4c3471f2 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -589,6 +589,4 @@ def test_no_warn_big_data_when_loc_specified(): with pytest.warns(None) as records: l = ax.legend('best') fig.canvas.draw() - # The _find_best_position method of Legend is called twice, duplicating - # the warning message. assert len(records) == 0 From 03654551a3a3707b3b86a79538fe91611829d132 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 17 Nov 2018 10:24:09 -0600 Subject: [PATCH 08/12] use rc_context, thanks @timhoffm for the tip --- lib/matplotlib/tests/test_legend.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index d17c4c3471f2..67c63ab10928 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -569,10 +569,8 @@ def test_warn_big_data_best_loc(): fig, ax = plt.subplots() ax.plot(np.arange(200001), label='Is this big data?') with pytest.warns(UserWarning) as records: - l = ax.legend() - # We need to call an internal set method tricking it into thinking - # 'best' location was default. - l._set_loc(l.codes['best'], is_initial_setting=True) + with rc_context({'legend.loc': 'best'}): + l = ax.legend() fig.canvas.draw() # The _find_best_position method of Legend is called twice, duplicating # the warning message. From f01b674463a3027f6341426addf3ac4eb94391fd Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 17 Nov 2018 10:41:37 -0600 Subject: [PATCH 09/12] forgot to import rc_context after rebase --- lib/matplotlib/tests/test_legend.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 67c63ab10928..2a41be4ad5d0 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -13,6 +13,7 @@ from matplotlib.legend_handler import HandlerTuple import matplotlib.legend as mlegend from matplotlib.cbook.deprecation import MatplotlibDeprecationWarning +from matplotlib import rc_context def test_legend_ordereddict(): From 2cb35505557edf1add41de3f1c395ce18e500617 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 21 Nov 2018 17:54:34 -0600 Subject: [PATCH 10/12] Incorporate @timhoffm style change Co-Authored-By: kbrose --- lib/matplotlib/legend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 52924da825cd..0a3a348d9aa9 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1118,7 +1118,7 @@ def _find_best_position(self, width, height, renderer, consider=None): if self._loc_used_default and verts.shape[0] > 200000: # this size results in a 3+ second render time on a good machine cbook._warn_external( - 'Creating legend with loc="best" can be slow with large' + 'Creating legend with loc="best" can be slow with large ' ' amounts of data.' ) From 282ac5340975716314b0f5c097b4081b728ff4d1 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 21 Nov 2018 17:54:46 -0600 Subject: [PATCH 11/12] Incorporate @timhoffm style change Co-Authored-By: kbrose --- lib/matplotlib/legend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 0a3a348d9aa9..2a44f9711a1c 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -1119,7 +1119,7 @@ def _find_best_position(self, width, height, renderer, consider=None): # this size results in a 3+ second render time on a good machine cbook._warn_external( 'Creating legend with loc="best" can be slow with large ' - ' amounts of data.' + 'amounts of data.' ) bbox = Bbox.from_bounds(0, 0, width, height) From f89a78cd4f74efb8d9c1cee1514a270214fcdd70 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Wed, 21 Nov 2018 19:02:09 -0500 Subject: [PATCH 12/12] Some more refactoring courtesy of @timhoffm --- lib/matplotlib/legend.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 2a44f9711a1c..b5bb459bab82 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -484,13 +484,11 @@ def __init__(self, parent, handles, labels, raise TypeError("Legend needs either Axes or Figure as parent") self.parent = parent + self._loc_used_default = loc is None if loc is None: - self._loc_used_default = True loc = rcParams["legend.loc"] if not self.isaxes and loc in [0, 'best']: loc = 'upper right' - else: - self._loc_used_default = False if isinstance(loc, str): if loc not in self.codes: if self.isaxes: @@ -571,7 +569,9 @@ def __init__(self, parent, handles, labels, else: self.get_frame().set_alpha(framealpha) - self._set_loc(loc, is_initial_setting=True) + tmp = self._loc_used_default + self._set_loc(loc) + self._loc_used_default = tmp # ignore changes done by _set_loc # figure out title fontsize: if title_fontsize is None: @@ -592,13 +592,11 @@ def _set_artist_props(self, a): a.set_transform(self.get_transform()) - def _set_loc(self, loc, is_initial_setting=False): + def _set_loc(self, loc): # find_offset function will be provided to _legend_box and # _legend_box will draw itself at the location of the return # value of the find_offset. - if not is_initial_setting: - # User manually changed self._loc - self._loc_used_default = False + self._loc_used_default = False self._loc_real = loc self.stale = True self._legend_box.set_offset(self._findoffset)