From 90692fe1c787ede589518bcfcee08b64ceef6469 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 17 Dec 2016 01:31:33 -0600 Subject: [PATCH 01/16] untested proof of concept --- lib/matplotlib/lines.py | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 5e9435a38ac2..4d4f2b431e30 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -229,6 +229,58 @@ def _slice_or_none(in_v, slc): 'markevery=%s' % (markevery,)) +def _reduce_path(tpath, ax): + """ + Helper function to compute the reduced path. + """ + # TODO: Ensure the following conditions: + # - monotonically increasing x values (???) + + # pull out the two bits of data we want from the path + codes, verts = tpath.codes, tpath.vertices + + def _slice_or_none(in_v, slc): + """ + Helper function to cope with `codes` being an + ndarray or `None` + """ + if in_v is None: + return None + return in_v[slc] + + x0, x1 = ax.get_xbound() + num_pixel_columns = np.diff(ax.transData.transform([[x0, 0], + [x1, 0]])[:,0]) + if verts.shape[0] <= num_pixel_columns * 4: + # Not worth reducing + return tpath + + # Convert vertices from data space to pixel space. + # TODO: Find out if this is already stored somewhere. + verts_trans = ax.transData.transform(verts) + x_pixel_columns = np.floor(verts_trans[:,0]).astype(int) + + # This approach assumes monotonically increasing x values! + unique_indices = np.unique(x_pixel_columns, return_index=True)[1] + keep_inds = np.ones((unique_indices.size, 4), dtype=int) * -1 + for i, x_pixel_column in enumerate(np.split(x_pixel_columns, + unique_indices[1:])): + # np.split already starts the first slice at 0, and unique_indices + # is guaranteed to start at 0, so skip the 0th element in the split. + pixel_col_start = unique_indices[i] + if i+1 == unique_indices.size: + pixel_col_end = x_pixel_columns.size + else: + pixel_col_end = unique_indices[i+1] + keep_inds[i] = (pixel_col_start, np.argmin(x_pixel_column), + np.argmax(x_pixel_column), pixel_col_end-1) + keep_inds = np.unique(keep_inds) + + # TODO: Develop approach that does not assume monotonicity. + + return Path(verts[keep_inds], _slice_or_none(codes, keep_inds)) + + class Line2D(Artist): """ A line - the line can have both a solid linestyle connecting all From c9563498c0a9b9075fcdd8b536bfc9d8810626ca Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 17 Dec 2016 12:33:19 -0600 Subject: [PATCH 02/16] renaming to avoid name clash with reduce --- lib/matplotlib/lines.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 4d4f2b431e30..b7a6a20f0af5 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -229,9 +229,9 @@ def _slice_or_none(in_v, slc): 'markevery=%s' % (markevery,)) -def _reduce_path(tpath, ax): +def _downsample_path(tpath, ax): """ - Helper function to compute the reduced path. + Helper function to compute the downsampled path. """ # TODO: Ensure the following conditions: # - monotonically increasing x values (???) @@ -258,22 +258,25 @@ def _slice_or_none(in_v, slc): # Convert vertices from data space to pixel space. # TODO: Find out if this is already stored somewhere. verts_trans = ax.transData.transform(verts) - x_pixel_columns = np.floor(verts_trans[:,0]).astype(int) # This approach assumes monotonically increasing x values! - unique_indices = np.unique(x_pixel_columns, return_index=True)[1] + unique_indices = np.unique(np.floor(verts_trans[:,0]), + return_index=True)[1] keep_inds = np.ones((unique_indices.size, 4), dtype=int) * -1 - for i, x_pixel_column in enumerate(np.split(x_pixel_columns, - unique_indices[1:])): + for i, y_pixel_col in enumerate(np.split(verts_trans[:,1], + unique_indices[1:])): # np.split already starts the first slice at 0, and unique_indices # is guaranteed to start at 0, so skip the 0th element in the split. pixel_col_start = unique_indices[i] if i+1 == unique_indices.size: - pixel_col_end = x_pixel_columns.size + pixel_col_end = verts_trans.shape[0] else: pixel_col_end = unique_indices[i+1] - keep_inds[i] = (pixel_col_start, np.argmin(x_pixel_column), - np.argmax(x_pixel_column), pixel_col_end-1) + keep_inds[i] = (pixel_col_start, + pixel_col_start + np.argmin(y_pixel_col), + pixel_col_start + np.argmax(y_pixel_col), + pixel_col_end-1) + # TODO: Not strictly necessary, worth doing benchmarks to see if worth it. keep_inds = np.unique(keep_inds) # TODO: Develop approach that does not assume monotonicity. @@ -644,6 +647,16 @@ def get_markevery(self): """return the markevery setting""" return self._markevery + def set_downsample(self, downsample): + """TODO""" + if self._downsample != downsample: + self.stale = True + self._downsample = downsample + + def get_downsample(self): + """return the downsample setting""" + return self._downsample + def set_picker(self, p): """Sets the event picker details for the line. @@ -827,6 +840,8 @@ def draw(self, renderer): if funcname != '_draw_nothing': tpath, affine = transf_path.get_transformed_path_and_affine() if len(tpath.vertices): + if self._downsample: + tpath = _downsample_path(tpath, self.axes) line_func = getattr(self, funcname) gc = renderer.new_gc() self._set_gc_clip(gc) @@ -875,6 +890,9 @@ def draw(self, renderer): marker = self._marker tpath, affine = transf_path.get_transformed_points_and_affine() if len(tpath.vertices): + if self._downsample: + tpath = _downsample_path(tpath, self.axes) + # subsample the markers if markevery is not None markevery = self.get_markevery() if markevery is not None: From 3cb6dd0c30a9ef0bb1f14a1a830ce60fd0ba2309 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 17 Dec 2016 13:06:21 -0600 Subject: [PATCH 03/16] initializing value of downsample --- lib/matplotlib/lines.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index b7a6a20f0af5..24c6e225d101 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -456,6 +456,7 @@ def __init__(self, xdata, ydata, self._marker = MarkerStyle(marker, fillstyle) self._markevery = None + self._downsample = None self._markersize = None self._antialiased = None From e55b1075b6100f5183b5e6b4d14d5d104f5d1428 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 17 Dec 2016 14:52:14 -0600 Subject: [PATCH 04/16] refactoring to bring _downsample_path into Line --- lib/matplotlib/lines.py | 163 +++++++++++++++++++++++++--------------- 1 file changed, 103 insertions(+), 60 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 24c6e225d101..506b2a0f2e32 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -229,61 +229,6 @@ def _slice_or_none(in_v, slc): 'markevery=%s' % (markevery,)) -def _downsample_path(tpath, ax): - """ - Helper function to compute the downsampled path. - """ - # TODO: Ensure the following conditions: - # - monotonically increasing x values (???) - - # pull out the two bits of data we want from the path - codes, verts = tpath.codes, tpath.vertices - - def _slice_or_none(in_v, slc): - """ - Helper function to cope with `codes` being an - ndarray or `None` - """ - if in_v is None: - return None - return in_v[slc] - - x0, x1 = ax.get_xbound() - num_pixel_columns = np.diff(ax.transData.transform([[x0, 0], - [x1, 0]])[:,0]) - if verts.shape[0] <= num_pixel_columns * 4: - # Not worth reducing - return tpath - - # Convert vertices from data space to pixel space. - # TODO: Find out if this is already stored somewhere. - verts_trans = ax.transData.transform(verts) - - # This approach assumes monotonically increasing x values! - unique_indices = np.unique(np.floor(verts_trans[:,0]), - return_index=True)[1] - keep_inds = np.ones((unique_indices.size, 4), dtype=int) * -1 - for i, y_pixel_col in enumerate(np.split(verts_trans[:,1], - unique_indices[1:])): - # np.split already starts the first slice at 0, and unique_indices - # is guaranteed to start at 0, so skip the 0th element in the split. - pixel_col_start = unique_indices[i] - if i+1 == unique_indices.size: - pixel_col_end = verts_trans.shape[0] - else: - pixel_col_end = unique_indices[i+1] - keep_inds[i] = (pixel_col_start, - pixel_col_start + np.argmin(y_pixel_col), - pixel_col_start + np.argmax(y_pixel_col), - pixel_col_end-1) - # TODO: Not strictly necessary, worth doing benchmarks to see if worth it. - keep_inds = np.unique(keep_inds) - - # TODO: Develop approach that does not assume monotonicity. - - return Path(verts[keep_inds], _slice_or_none(codes, keep_inds)) - - class Line2D(Artist): """ A line - the line can have both a solid linestyle connecting all @@ -365,6 +310,7 @@ def __init__(self, xdata, ydata, pickradius=5, drawstyle=None, markevery=None, + downsample=None, **kwargs ): """ @@ -461,6 +407,7 @@ def __init__(self, xdata, ydata, self._antialiased = None self.set_markevery(markevery) + self.set_downsample(downsample) self.set_antialiased(antialiased) self.set_markersize(markersize) @@ -639,6 +586,10 @@ def set_markevery(self, every): markers is always determined from the display-coordinates axes-bounding-box-diagonal regardless of the actual axes data limits. + See also + -------- + downsample : for downsampling the line segments that are plotted. + """ if self._markevery != every: self.stale = True @@ -649,7 +600,50 @@ def get_markevery(self): return self._markevery def set_downsample(self, downsample): - """TODO""" + """Set the downsample property to subsample the plotted line segments. + + If True, then only a subset of line segments will be plotted. The + subset is carefully chosen so that the rendering of the downsampled + plot looks very similar to the rending of the non-downsampled plot. In + fact, if there is no anti-aliasing and plotting uses a solid line only, + then the renderings will be identical. + + Recommended *only* when plotting with a solid line (e.g. no dashes, + no markers). + + ACCEPTS: [True | False] + + Parameters + ---------- + downsample: None | boolean-like + Whether or not to downsample. + + - downsample=True, a downsampled set of points will be plotted. + - downsample=False, every point will be plotted. + + Notes + ----- + If the x values of the line are monotonic, then the downsampling + algorithm will plot at most 4 times the width of the parent + :class:`matplotlib.axes.Axes` in pixels of vertices. If the x values + are not monotonic, the plot will still render correctly, but you are + not guaranteed any faster rendering. + + If plotting with a non-solid line format (e.g. '--'), then + the downsampled plot could render quite differently from the + non-downsampled plot. + + Markers will not be downsampled. + + If the line is anti-aliased, then the downsampled plot will not + render *exactly* the same as a non-dowsampled plot, + but will be very similar. + + See also + -------- + markevery : for downsampling the markers that are plotted. + + """ if self._downsample != downsample: self.stale = True self._downsample = downsample @@ -813,6 +807,58 @@ def _is_sorted(self, x): # We don't handle the monotonically decreasing case. return _path.is_sorted(x) + def _downsample_path(self, tpath): + """ + Helper function to compute the downsampled path. + """ + # pull out the two bits of data we want from the path + codes, verts = tpath.codes, tpath.vertices + + def _slice_or_none(in_v, slc): + """ + Helper function to cope with `codes` being an ndarray or `None` + """ + if in_v is None: + return None + return in_v[slc] + + # Don't waste time downsampling if it won't give us any benefit + x0, x1 = self.axes.get_xbound() + pixel_width = np.diff(self.axes.transData.transform([[x0, 0], + [x1, 0]])[:,0]) + if verts.shape[0] <= pixel_width * 4: + return tpath + + # Convert vertices from data space to pixel space. + # TODO: Find out if this is already stored somewhere. + verts_trans = self.axes.transData.transform(verts) + + # Find where the pixel column of the x data changes + split_indices = np.diff(np.floor(verts_trans[:,0]).astype(int)) != 0 + split_indices = np.where(split_indices)[0]+1 + + keep_inds = np.zeros((split_indices.size + 1, 4), dtype=int) + for i, y_pixel_col in enumerate(np.split(verts_trans[:,1], + split_indices)): + if i == 0: + pixel_col_start = 0 + else: + pixel_col_start = split_indices[i-1] + + if i == split_indices.size: + pixel_col_end = verts_trans.shape[0] + else: + pixel_col_end = split_indices[i] + + keep_inds[i] = (pixel_col_start, + pixel_col_start + np.argmin(y_pixel_col), + pixel_col_start + np.argmax(y_pixel_col), + pixel_col_end-1) + + keep_inds = keep_inds.flatten() + + return Path(verts[keep_inds], _slice_or_none(codes, keep_inds)) + @allow_rasterization def draw(self, renderer): """draw the Line with `renderer` unless visibility is False""" @@ -842,7 +888,7 @@ def draw(self, renderer): tpath, affine = transf_path.get_transformed_path_and_affine() if len(tpath.vertices): if self._downsample: - tpath = _downsample_path(tpath, self.axes) + tpath = self._downsample_path(tpath) line_func = getattr(self, funcname) gc = renderer.new_gc() self._set_gc_clip(gc) @@ -891,9 +937,6 @@ def draw(self, renderer): marker = self._marker tpath, affine = transf_path.get_transformed_points_and_affine() if len(tpath.vertices): - if self._downsample: - tpath = _downsample_path(tpath, self.axes) - # subsample the markers if markevery is not None markevery = self.get_markevery() if markevery is not None: From 99883edd0c213e9828163598531dffdc1eaa11c1 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 17 Dec 2016 15:54:16 -0600 Subject: [PATCH 05/16] adding test --- lib/matplotlib/tests/test_lines.py | 33 +++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index dd87ffe8b0bf..7c3d6017868b 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -8,7 +8,7 @@ import itertools import matplotlib.lines as mlines import nose -from nose.tools import assert_true, assert_raises +from nose.tools import assert_true, assert_raises, assert_equal from timeit import repeat import numpy as np from cycler import cycler @@ -191,5 +191,36 @@ def test_nan_is_sorted(): assert_true(not line._is_sorted([3, 5] + [np.nan] * 100 + [0, 2])) +@cleanup +def test_downsample_lines(): + x = np.array([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1.0]) + y = np.array([2, 8, 3, 6, 1, 5, 5, 8, 7, 6, 6, 7.0]) + + line = mlines.Line2D(x, y, downsample=True) + + fig, ax = plt.subplots() + + # Adjust size of figure to ensure downsampling occurs + fig.set_dpi(1) + fig.set_size_inches([2,1]) + + ax.add_line(line) + + tpath, _ = line._get_transformed_path().get_transformed_path_and_affine() + down_verts = line._downsample_path(tpath).vertices + + expected_down_verts = np.array([ + [0.0, 2.0], + [0.0, 1.0], + [0.0, 8.0], + [0.0, 5.0], + [1.0, 5.0], + [1.0, 5.0], + [1.0, 8.0], + [1.0, 7.0] + ]) + assert_equal(down_verts, expected_down_verts) + + if __name__ == '__main__': nose.runmodule(argv=['-s', '--with-doctest'], exit=False) From 624786bc75af8dea4921c83a283921704226b24e Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 17 Dec 2016 16:08:24 -0600 Subject: [PATCH 06/16] adding example --- examples/pyplots/pyplot_downsample.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 examples/pyplots/pyplot_downsample.py diff --git a/examples/pyplots/pyplot_downsample.py b/examples/pyplots/pyplot_downsample.py new file mode 100644 index 000000000000..295cb1d7bcba --- /dev/null +++ b/examples/pyplots/pyplot_downsample.py @@ -0,0 +1,21 @@ +import numpy as np +import matplotlib.pyplot as plt + +# Fixing random state for reproducibility +np.random.seed(8675309) + +# make up some data +y = np.random.normal(loc=0.5, scale=0.4, size=1000000) +x = np.arange(len(y)) + +# plot without downsampling +plt.figure(1) +plt.plot(x, y) + +# plot with downsampling +plt.figure(2) +plt.plot(x, y, downsample=True) + +plt.show() + +# interact with both figures to compare snapiness From b6c4b2c098366508d76f7787a03fecdbf3dfcb5b Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sun, 18 Dec 2016 02:00:00 -0600 Subject: [PATCH 07/16] fixing nose test for np array equality --- lib/matplotlib/tests/test_lines.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 7c3d6017868b..6a4c9aebb67e 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -8,7 +8,7 @@ import itertools import matplotlib.lines as mlines import nose -from nose.tools import assert_true, assert_raises, assert_equal +from nose.tools import assert_true, assert_raises from timeit import repeat import numpy as np from cycler import cycler @@ -193,8 +193,8 @@ def test_nan_is_sorted(): @cleanup def test_downsample_lines(): - x = np.array([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1.0]) - y = np.array([2, 8, 3, 6, 1, 5, 5, 8, 7, 6, 6, 7.0]) + x = np.array([0.0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]) + y = np.array([2.0, 8, 3, 6, 1, 5, 5, 8, 7, 6, 6, 7]) line = mlines.Line2D(x, y, downsample=True) @@ -202,7 +202,7 @@ def test_downsample_lines(): # Adjust size of figure to ensure downsampling occurs fig.set_dpi(1) - fig.set_size_inches([2,1]) + fig.set_size_inches([2, 1]) ax.add_line(line) @@ -219,7 +219,7 @@ def test_downsample_lines(): [1.0, 8.0], [1.0, 7.0] ]) - assert_equal(down_verts, expected_down_verts) + assert_true(np.all(down_verts == expected_down_verts)) if __name__ == '__main__': From 86f161247580686b694a9e367e79a905193231ef Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sun, 18 Dec 2016 13:55:27 -0600 Subject: [PATCH 08/16] pep8 conformance --- lib/matplotlib/lines.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 506b2a0f2e32..a12a32f0f2b7 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -825,7 +825,7 @@ def _slice_or_none(in_v, slc): # Don't waste time downsampling if it won't give us any benefit x0, x1 = self.axes.get_xbound() pixel_width = np.diff(self.axes.transData.transform([[x0, 0], - [x1, 0]])[:,0]) + [x1, 0]])[:, 0]) if verts.shape[0] <= pixel_width * 4: return tpath @@ -834,16 +834,16 @@ def _slice_or_none(in_v, slc): verts_trans = self.axes.transData.transform(verts) # Find where the pixel column of the x data changes - split_indices = np.diff(np.floor(verts_trans[:,0]).astype(int)) != 0 - split_indices = np.where(split_indices)[0]+1 + split_indices = np.diff(np.floor(verts_trans[:, 0]).astype(int)) != 0 + split_indices = np.where(split_indices)[0] + 1 keep_inds = np.zeros((split_indices.size + 1, 4), dtype=int) - for i, y_pixel_col in enumerate(np.split(verts_trans[:,1], + for i, y_pixel_col in enumerate(np.split(verts_trans[:, 1], split_indices)): if i == 0: pixel_col_start = 0 else: - pixel_col_start = split_indices[i-1] + pixel_col_start = split_indices[i - 1] if i == split_indices.size: pixel_col_end = verts_trans.shape[0] @@ -853,7 +853,7 @@ def _slice_or_none(in_v, slc): keep_inds[i] = (pixel_col_start, pixel_col_start + np.argmin(y_pixel_col), pixel_col_start + np.argmax(y_pixel_col), - pixel_col_end-1) + pixel_col_end - 1) keep_inds = keep_inds.flatten() From e6b62a196b897a06503e61ac132d37968b04889f Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sun, 18 Dec 2016 14:45:14 -0600 Subject: [PATCH 09/16] adding property, clean up docstring --- lib/matplotlib/lines.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index a12a32f0f2b7..3eb5e40346e3 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -615,7 +615,7 @@ def set_downsample(self, downsample): Parameters ---------- - downsample: None | boolean-like + downsample: True | False Whether or not to downsample. - downsample=True, a downsampled set of points will be plotted. @@ -652,6 +652,8 @@ def get_downsample(self): """return the downsample setting""" return self._downsample + downsample = property(get_downsample, set_downsample) + def set_picker(self, p): """Sets the event picker details for the line. From b11984ec51c2122b8a7d363dc2c6041e24e395b8 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sun, 18 Dec 2016 20:13:00 -0600 Subject: [PATCH 10/16] simplifying loop per suggestion from @efiring --- lib/matplotlib/lines.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 3eb5e40346e3..bdcbaed4ed6f 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -842,20 +842,12 @@ def _slice_or_none(in_v, slc): keep_inds = np.zeros((split_indices.size + 1, 4), dtype=int) for i, y_pixel_col in enumerate(np.split(verts_trans[:, 1], split_indices)): - if i == 0: - pixel_col_start = 0 - else: - pixel_col_start = split_indices[i - 1] - - if i == split_indices.size: - pixel_col_end = verts_trans.shape[0] - else: - pixel_col_end = split_indices[i] + keep_inds[i, 1:3] = np.argmin(y_pixel_col), np.argmax(y_pixel_col) - keep_inds[i] = (pixel_col_start, - pixel_col_start + np.argmin(y_pixel_col), - pixel_col_start + np.argmax(y_pixel_col), - pixel_col_end - 1) + starts = np.hstack((0, split_indices)) + ends = np.hstack((split_indices, verts_trans.shape[0])) + keep_inds[:, :3] += starts[:, np.newaxis] + keep_inds[:, 3] = ends - 1 keep_inds = keep_inds.flatten() From 445a051b5f265233b442839938123eda52e53ad6 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sun, 18 Dec 2016 21:34:44 -0600 Subject: [PATCH 11/16] using property decorator, works for non-linear axes scales --- lib/matplotlib/lines.py | 27 ++++++++++++++------------- lib/matplotlib/tests/test_lines.py | 4 ++-- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index bdcbaed4ed6f..3962aafeb827 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -402,15 +402,15 @@ def __init__(self, xdata, ydata, self._marker = MarkerStyle(marker, fillstyle) self._markevery = None - self._downsample = None self._markersize = None self._antialiased = None self.set_markevery(markevery) - self.set_downsample(downsample) self.set_antialiased(antialiased) self.set_markersize(markersize) + self._downsample = downsample + self._markeredgecolor = None self._markeredgewidth = None self._markerfacecolor = None @@ -599,7 +599,8 @@ def get_markevery(self): """return the markevery setting""" return self._markevery - def set_downsample(self, downsample): + @property + def downsample(self): """Set the downsample property to subsample the plotted line segments. If True, then only a subset of line segments will be plotted. The @@ -644,16 +645,15 @@ def set_downsample(self, downsample): markevery : for downsampling the markers that are plotted. """ + return self._downsample + + @downsample.setter + def downsample(self, downsample): + """Sets the downsample property.""" if self._downsample != downsample: self.stale = True self._downsample = downsample - def get_downsample(self): - """return the downsample setting""" - return self._downsample - - downsample = property(get_downsample, set_downsample) - def set_picker(self, p): """Sets the event picker details for the line. @@ -809,7 +809,7 @@ def _is_sorted(self, x): # We don't handle the monotonically decreasing case. return _path.is_sorted(x) - def _downsample_path(self, tpath): + def _downsample_path(self, tpath, affine): """ Helper function to compute the downsampled path. """ @@ -832,8 +832,9 @@ def _slice_or_none(in_v, slc): return tpath # Convert vertices from data space to pixel space. - # TODO: Find out if this is already stored somewhere. - verts_trans = self.axes.transData.transform(verts) + # Any non-affine axis transformation has already been applied + # to tpath, so we just need to apply affine part. + verts_trans = affine.transform_path(tpath).vertices # Find where the pixel column of the x data changes split_indices = np.diff(np.floor(verts_trans[:, 0]).astype(int)) != 0 @@ -882,7 +883,7 @@ def draw(self, renderer): tpath, affine = transf_path.get_transformed_path_and_affine() if len(tpath.vertices): if self._downsample: - tpath = self._downsample_path(tpath) + tpath = self._downsample_path(tpath, affine) line_func = getattr(self, funcname) gc = renderer.new_gc() self._set_gc_clip(gc) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 6a4c9aebb67e..d01d92c19b32 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -206,8 +206,8 @@ def test_downsample_lines(): ax.add_line(line) - tpath, _ = line._get_transformed_path().get_transformed_path_and_affine() - down_verts = line._downsample_path(tpath).vertices + tpath, affine = line._get_transformed_path().get_transformed_path_and_affine() + down_verts = line._downsample_path(tpath, affine).vertices expected_down_verts = np.array([ [0.0, 2.0], From d460c9090fe234fb6509b5a65ce5ced723d6fc1a Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Mon, 19 Dec 2016 01:10:22 -0600 Subject: [PATCH 12/16] guarding against NaN y-values and non-monotonic x-values --- lib/matplotlib/lines.py | 22 ++++++++++++++-------- lib/matplotlib/tests/test_lines.py | 26 ++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 3962aafeb827..7a4feda89f62 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -824,13 +824,6 @@ def _slice_or_none(in_v, slc): return None return in_v[slc] - # Don't waste time downsampling if it won't give us any benefit - x0, x1 = self.axes.get_xbound() - pixel_width = np.diff(self.axes.transData.transform([[x0, 0], - [x1, 0]])[:, 0]) - if verts.shape[0] <= pixel_width * 4: - return tpath - # Convert vertices from data space to pixel space. # Any non-affine axis transformation has already been applied # to tpath, so we just need to apply affine part. @@ -840,10 +833,23 @@ def _slice_or_none(in_v, slc): split_indices = np.diff(np.floor(verts_trans[:, 0]).astype(int)) != 0 split_indices = np.where(split_indices)[0] + 1 + # Don't waste time downsampling if it won't give us any benefit + # 4.0 was chosen somewhat arbitrarily. + if split_indices.size >= verts_trans.shape[0] / 4.0: + return tpath + keep_inds = np.zeros((split_indices.size + 1, 4), dtype=int) for i, y_pixel_col in enumerate(np.split(verts_trans[:, 1], split_indices)): - keep_inds[i, 1:3] = np.argmin(y_pixel_col), np.argmax(y_pixel_col) + try: + keep_inds[i, 1:3] = (np.nanargmin(y_pixel_col), + np.nanargmax(y_pixel_col)) + except ValueError: + # np.nanarg* raises a ValueError if all elements are NaN. + # If either np.nanargmin or np.nanargmax raise this error, + # then all y_pixel_col is NaN. In this case, the argmin + # and argmax are both undefined, just keep the first value. + keep_inds[i, 1:3] = 0 starts = np.hstack((0, split_indices)) ends = np.hstack((split_indices, verts_trans.shape[0])) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index d01d92c19b32..67e83fa19e1a 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -193,8 +193,9 @@ def test_nan_is_sorted(): @cleanup def test_downsample_lines(): - x = np.array([0.0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]) - y = np.array([2.0, 8, 3, 6, 1, 5, 5, 8, 7, 6, 6, 7]) + x = np.hstack((np.zeros(99), np.ones(99))) + y = np.hstack([2.0, 8.0, 3 * np.ones(95), 1.0, 5.0, + 5.0, 5.5, 6 * np.ones(95), 8.0, 7.0]) line = mlines.Line2D(x, y, downsample=True) @@ -222,5 +223,26 @@ def test_downsample_lines(): assert_true(np.all(down_verts == expected_down_verts)) +@cleanup +def test_downsample_nan_lines(): + # Creates x and y data, add some NaN values + N = 10**5 + x = np.linspace(0,1,N) + y = np.random.normal(size=N) + x[10000:30000] = np.nan + y[20000:40000] = np.nan + + line = mlines.Line2D(x, y, downsample=True) + + fig, ax = plt.subplots() + + ax.add_line(line) + + fig.canvas.draw() + + # Nothing to assert here, we are just making sure NaN values + # do not raise any errors. + + if __name__ == '__main__': nose.runmodule(argv=['-s', '--with-doctest'], exit=False) From 5a9bc7a832080a86d3cc653ce987f21f16427fed Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Mon, 19 Dec 2016 22:31:05 -0600 Subject: [PATCH 13/16] updating test to reflect changes --- lib/matplotlib/tests/test_lines.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 67e83fa19e1a..13a343401d9b 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -199,15 +199,12 @@ def test_downsample_lines(): line = mlines.Line2D(x, y, downsample=True) - fig, ax = plt.subplots() - - # Adjust size of figure to ensure downsampling occurs - fig.set_dpi(1) - fig.set_size_inches([2, 1]) + _, ax = plt.subplots() ax.add_line(line) - tpath, affine = line._get_transformed_path().get_transformed_path_and_affine() + transf_path = line._get_transformed_path() + tpath, affine = transf_path.get_transformed_path_and_affine() down_verts = line._downsample_path(tpath, affine).vertices expected_down_verts = np.array([ From 0fd581b0fcd711ae1604a91a21fe769d010289a3 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 31 Dec 2016 15:51:01 -0600 Subject: [PATCH 14/16] adding docstring to the example --- examples/pyplots/pyplot_downsample.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/pyplots/pyplot_downsample.py b/examples/pyplots/pyplot_downsample.py index 295cb1d7bcba..00550fb2190b 100644 --- a/examples/pyplots/pyplot_downsample.py +++ b/examples/pyplots/pyplot_downsample.py @@ -1,3 +1,12 @@ +""" +Demo of downsampling line plots. + +This method of downsampling will greatly speed up interaction +and rendering time, without changing the actual rasterization too much. +Try interacting with the two figures (panning, zooming, etc.) to see +the difference. + +""" import numpy as np import matplotlib.pyplot as plt From f9d359b3711875bcf05adbb4b28c5b2d35438896 Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 31 Dec 2016 15:51:26 -0600 Subject: [PATCH 15/16] downsample docstring cleanups --- lib/matplotlib/lines.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 7a4feda89f62..5b774f18abe8 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -612,11 +612,9 @@ def downsample(self): Recommended *only* when plotting with a solid line (e.g. no dashes, no markers). - ACCEPTS: [True | False] - Parameters ---------- - downsample: True | False + downsample : True | False Whether or not to downsample. - downsample=True, a downsampled set of points will be plotted. @@ -624,9 +622,9 @@ def downsample(self): Notes ----- - If the x values of the line are monotonic, then the downsampling + If the x-values of the line are monotonic, then the downsampling algorithm will plot at most 4 times the width of the parent - :class:`matplotlib.axes.Axes` in pixels of vertices. If the x values + :class:`matplotlib.axes.Axes` in pixels of vertices. If the x-values are not monotonic, the plot will still render correctly, but you are not guaranteed any faster rendering. From 53f5ca95de74f58b19c95a641727e332db9a4fca Mon Sep 17 00:00:00 2001 From: Kevin Rose Date: Sat, 31 Dec 2016 16:30:14 -0600 Subject: [PATCH 16/16] adding a whats-new description --- doc/users/whats_new/plot_downsample.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/users/whats_new/plot_downsample.rst diff --git a/doc/users/whats_new/plot_downsample.rst b/doc/users/whats_new/plot_downsample.rst new file mode 100644 index 000000000000..8959760c0eb8 --- /dev/null +++ b/doc/users/whats_new/plot_downsample.rst @@ -0,0 +1,7 @@ +Downsample line plots +--------------------- + +A ``downsample`` parameter now exists for the method :func:`plot`. This +allows line plots to be intelligently downsampled so that rendering and +interaction is faster. The downsampling algorithm will render an image +very similar to a non-downsampled image.