diff --git a/doc/api/lines_api.rst b/doc/api/lines_api.rst index 808df726d118..94af43cf6feb 100644 --- a/doc/api/lines_api.rst +++ b/doc/api/lines_api.rst @@ -17,6 +17,7 @@ Classes Line2D VertexSelector + Line2DWithErrorbars Functions --------- @@ -26,4 +27,3 @@ Functions :template: autosummary.rst segment_hits - \ No newline at end of file diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 35fd2b0ce890..ea21a75eb716 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3211,7 +3211,8 @@ def errorbar(self, x, y, yerr=None, xerr=None, `.ErrorbarContainer` The container contains: - - plotline: `.Line2D` instance of x, y plot markers and/or line. + - plotline: `.Line2DWithErrorbars` instance of x, y plot markers + and/or line. - caplines: A tuple of `.Line2D` instances of the error bar caps. - barlinecols: A tuple of `.LineCollection` with the horizontal and vertical error ranges. @@ -3239,7 +3240,6 @@ def errorbar(self, x, y, yerr=None, xerr=None, # anything that comes in as 'None', drop so the default thing # happens down stream kwargs = {k: v for k, v in kwargs.items() if v is not None} - kwargs.setdefault('zorder', 2) try: offset, errorevery = errorevery @@ -3301,8 +3301,6 @@ def errorbar(self, x, y, yerr=None, xerr=None, plot_line_style = { **base_style, **kwargs, - 'zorder': (kwargs['zorder'] - .1 if barsabove else - kwargs['zorder'] + .1), } # make the style dict for the line collections (the bars) @@ -3344,13 +3342,9 @@ def errorbar(self, x, y, yerr=None, xerr=None, eb_cap_style[key] = kwargs[key] eb_cap_style['color'] = ecolor - data_line = None - if plot_line: - data_line = mlines.Line2D(x, y, **plot_line_style) - self.add_line(data_line) - - barcols = [] - caplines = [] + eb_line = mlines.Line2DWithErrorbars( + x, y, barsabove=barsabove, plot_line=plot_line, **plot_line_style) + self.add_line(eb_line) # arrays fine here, they are booleans and hence not units lolims = np.broadcast_to(lolims, len(x)).astype(bool) @@ -3408,6 +3402,92 @@ def extract_err(err, data): high = [v + e for v, e in zip(data, b)] return low, high + def eb_hlines(y, xmin, xmax, colors='k', linestyles='solid', + label='', **kwargs): + """Private function like hlines, but not adding lines to Axes""" + self._process_unit_info([xmin, xmax], y, kwargs=kwargs) + y = self.convert_yunits(y) + xmin = self.convert_xunits(xmin) + xmax = self.convert_xunits(xmax) + + if not np.iterable(y): + y = [y] + if not np.iterable(xmin): + xmin = [xmin] + if not np.iterable(xmax): + xmax = [xmax] + + # Create and combine masked_arrays from input + y, xmin, xmax = cbook._combine_masks(y, xmin, xmax) + y = np.ravel(y) + xmin = np.ravel(xmin) + xmax = np.ravel(xmax) + + masked_verts = np.ma.empty((len(y), 2, 2)) + masked_verts[:, 0, 0] = xmin + masked_verts[:, 0, 1] = y + masked_verts[:, 1, 0] = xmax + masked_verts[:, 1, 1] = y + + if len(y) > 0: + minx = min(xmin.min(), xmax.min()) + maxx = max(xmin.max(), xmax.max()) + miny = y.min() + maxy = y.max() + + corners = (minx, miny), (maxx, maxy) + self.update_datalim(corners) + self._request_autoscale_view() + + lines = mcoll.LineCollection(masked_verts, colors=colors, + linestyles=linestyles, label=label) + lines.update(kwargs) + return lines + + def eb_vlines(x, ymin, ymax, colors='k', linestyles='solid', + label='', **kwargs): + """Private function like vlines, but not adding lines to Axes""" + self._process_unit_info(xdata=x, ydata=[ymin, ymax], kwargs=kwargs) + + # We do the conversion first since not all unitized data is uniform + x = self.convert_xunits(x) + ymin = self.convert_yunits(ymin) + ymax = self.convert_yunits(ymax) + + if not np.iterable(x): + x = [x] + if not np.iterable(ymin): + ymin = [ymin] + if not np.iterable(ymax): + ymax = [ymax] + + # Create and combine masked_arrays from input + x, ymin, ymax = cbook._combine_masks(x, ymin, ymax) + x = np.ravel(x) + ymin = np.ravel(ymin) + ymax = np.ravel(ymax) + + masked_verts = np.ma.empty((len(x), 2, 2)) + masked_verts[:, 0, 0] = x + masked_verts[:, 0, 1] = ymin + masked_verts[:, 1, 0] = x + masked_verts[:, 1, 1] = ymax + + if len(x) > 0: + minx = x.min() + maxx = x.max() + miny = min(ymin.min(), ymax.min()) + maxy = max(ymin.max(), ymax.max()) + + corners = (minx, miny), (maxx, maxy) + self.update_datalim(corners) + self._request_autoscale_view() + + lines = mcoll.LineCollection(masked_verts, colors=colors, + linestyles=linestyles, label=label) + lines.update(kwargs) + return lines + if xerr is not None: left, right = extract_err(xerr, x) # select points without upper/lower limits in x and @@ -3416,46 +3496,46 @@ def extract_err(err, data): if noxlims.any() or len(noxlims) == 0: yo, _ = xywhere(y, right, noxlims & everymask) lo, ro = xywhere(left, right, noxlims & everymask) - barcols.append(self.hlines(yo, lo, ro, **eb_lines_style)) + eb_line.add_barcols(eb_hlines(yo, lo, ro, **eb_lines_style)) if capsize > 0: - caplines.append(mlines.Line2D(lo, yo, marker='|', - **eb_cap_style)) - caplines.append(mlines.Line2D(ro, yo, marker='|', - **eb_cap_style)) + eb_line.add_caplines(mlines.Line2D(lo, yo, marker='|', + **eb_cap_style)) + eb_line.add_caplines(mlines.Line2D(ro, yo, marker='|', + **eb_cap_style)) if xlolims.any(): yo, _ = xywhere(y, right, xlolims & everymask) lo, ro = xywhere(x, right, xlolims & everymask) - barcols.append(self.hlines(yo, lo, ro, **eb_lines_style)) + eb_line.add_barcols(eb_hlines(yo, lo, ro, **eb_lines_style)) rightup, yup = xywhere(right, y, xlolims & everymask) if self.xaxis_inverted(): marker = mlines.CARETLEFTBASE else: marker = mlines.CARETRIGHTBASE - caplines.append( + eb_line.add_caplines( mlines.Line2D(rightup, yup, ls='None', marker=marker, **eb_cap_style)) if capsize > 0: xlo, ylo = xywhere(x, y, xlolims & everymask) - caplines.append(mlines.Line2D(xlo, ylo, marker='|', - **eb_cap_style)) + eb_line.add_caplines(mlines.Line2D(xlo, ylo, marker='|', + **eb_cap_style)) if xuplims.any(): yo, _ = xywhere(y, right, xuplims & everymask) lo, ro = xywhere(left, x, xuplims & everymask) - barcols.append(self.hlines(yo, lo, ro, **eb_lines_style)) + eb_line.add_barcols(eb_hlines(yo, lo, ro, **eb_lines_style)) leftlo, ylo = xywhere(left, y, xuplims & everymask) if self.xaxis_inverted(): marker = mlines.CARETRIGHTBASE else: marker = mlines.CARETLEFTBASE - caplines.append( + eb_line.add_caplines( mlines.Line2D(leftlo, ylo, ls='None', marker=marker, **eb_cap_style)) if capsize > 0: xup, yup = xywhere(x, y, xuplims & everymask) - caplines.append(mlines.Line2D(xup, yup, marker='|', - **eb_cap_style)) + eb_line.add_caplines(mlines.Line2D(xup, yup, marker='|', + **eb_cap_style)) if yerr is not None: lower, upper = extract_err(yerr, y) @@ -3465,52 +3545,64 @@ def extract_err(err, data): if noylims.any() or len(noylims) == 0: xo, _ = xywhere(x, lower, noylims & everymask) lo, uo = xywhere(lower, upper, noylims & everymask) - barcols.append(self.vlines(xo, lo, uo, **eb_lines_style)) + eb_line.add_barcols(eb_vlines(xo, lo, uo, **eb_lines_style)) if capsize > 0: - caplines.append(mlines.Line2D(xo, lo, marker='_', - **eb_cap_style)) - caplines.append(mlines.Line2D(xo, uo, marker='_', - **eb_cap_style)) + eb_line.add_caplines(mlines.Line2D(xo, lo, marker='_', + **eb_cap_style)) + eb_line.add_caplines(mlines.Line2D(xo, uo, marker='_', + **eb_cap_style)) if lolims.any(): xo, _ = xywhere(x, lower, lolims & everymask) lo, uo = xywhere(y, upper, lolims & everymask) - barcols.append(self.vlines(xo, lo, uo, **eb_lines_style)) + eb_line.add_barcols(eb_vlines(xo, lo, uo, **eb_lines_style)) xup, upperup = xywhere(x, upper, lolims & everymask) if self.yaxis_inverted(): marker = mlines.CARETDOWNBASE else: marker = mlines.CARETUPBASE - caplines.append( + eb_line.add_caplines( mlines.Line2D(xup, upperup, ls='None', marker=marker, **eb_cap_style)) if capsize > 0: xlo, ylo = xywhere(x, y, lolims & everymask) - caplines.append(mlines.Line2D(xlo, ylo, marker='_', - **eb_cap_style)) + eb_line.add_caplines(mlines.Line2D(xlo, ylo, marker='_', + **eb_cap_style)) if uplims.any(): xo, _ = xywhere(x, lower, uplims & everymask) lo, uo = xywhere(lower, y, uplims & everymask) - barcols.append(self.vlines(xo, lo, uo, **eb_lines_style)) + eb_line.add_barcols(eb_vlines(xo, lo, uo, **eb_lines_style)) xlo, lowerlo = xywhere(x, lower, uplims & everymask) if self.yaxis_inverted(): marker = mlines.CARETUPBASE else: marker = mlines.CARETDOWNBASE - caplines.append( + eb_line.add_caplines( mlines.Line2D(xlo, lowerlo, ls='None', marker=marker, **eb_cap_style)) if capsize > 0: xup, yup = xywhere(x, y, uplims & everymask) - caplines.append(mlines.Line2D(xup, yup, marker='_', - **eb_cap_style)) - for l in caplines: - self.add_line(l) + eb_line.add_caplines(mlines.Line2D(xup, yup, marker='_', + **eb_cap_style)) + + for cl in eb_line._caplines: + self._update_line_limits(cl) + if cl.get_clip_path() is None: + cl.set_clip_path(self.patch) + if cl.mouseover: + self._mouseover_set.add(cl) + for bc in eb_line._barcols: + if bc.get_clip_path() is None: + bc.set_clip_path(self.patch) + if bc.mouseover: + self._mouseover_set.add(bc) + self.stale = True self._request_autoscale_view() - errorbar_container = ErrorbarContainer((data_line, tuple(caplines), - tuple(barcols)), + errorbar_container = ErrorbarContainer((eb_line if plot_line else None, + tuple(eb_line._caplines), + tuple(eb_line._barcols)), has_xerr=(xerr is not None), has_yerr=(yerr is not None), label=label) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index c13cc16ea44e..8fa4b47dc793 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1562,3 +1562,83 @@ def onpick(self, event): # You can not set the docstring of an instancemethod, # but you can on the underlying function. Go figure. docstring.dedent_interpd(Line2D.__init__) + + +class Line2DWithErrorbars(Line2D): + """ + A helper class disguised as a `.Line2D` holding the errorbar elements. + """ + def __init__(self, *args, barsabove=True, plot_line=True, **kwargs): + """ + Create a `.Line2D`-like object holding all the `.Artists` required + to draw errorbars. + + All `.Line2D` parameters are accepted. Additional parameters are: + + barsabove: bool, optional + Specify the order errorbars are plot with respect to line markers. + plot_line: bool, optional + Configure if line (and the respective markers) will be displayed. + """ + super().__init__(*args, **kwargs) + self._barcols = [] + self._caplines = [] + self._barsabove = barsabove + self._plot_line = plot_line + + def _set_artist_props(self, a): + """Set the boilerplate props for child artists.""" + a.set_figure(self.figure) + if not a.is_transform_set(): + a.set_transform(self.get_transform()) + a.axes = self.axes + + def _remove_caplines(self, caplines): + """Helper function to remove caplines, just for internal use.""" + self._caplines.remove(caplines) + if (not self._caplines and not self._barcols and + not self._plot_line): + self.remove() + + def _remove_barcols(self, barcols): + """Helper function to remove barcols, just for internal use.""" + self._barcols.remove(barcols) + if (not self._caplines and not self._barcols and + not self._plot_line): + self.remove() + + def add_caplines(self, caplines): + """Add a `.LineCollection` holding caplines information.""" + caplines._remove_method = self._remove_caplines + self._caplines.append(caplines) + + def add_barcols(self, barcols): + """Add a `.Line2D` holding barcols information.""" + barcols._remove_method = self._remove_barcols + self._barcols.append(barcols) + + @allow_rasterization + def draw(self, renderer): + # docstring inherited + if self._barsabove: + if self._plot_line: + super().draw(renderer) + for c in self.get_children(): + self._set_artist_props(c) + c.draw(renderer) + else: + for c in self.get_children(): + self._set_artist_props(c) + c.draw(renderer) + if self._plot_line: + super().draw(renderer) + + def get_children(self): + # docstring inherited + return [*self._barcols, *self._caplines] + + def remove(self): + # docstring inherited + self._plot_line = False + if (not self._caplines and not self._barcols): + super().remove() diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf b/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf index 7930f6d68249..228d4a135d35 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf and b/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.png b/lib/matplotlib/tests/baseline_images/test_legend/fancy.png index aba46e19e727..31893198a378 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_legend/fancy.png and b/lib/matplotlib/tests/baseline_images/test_legend/fancy.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg b/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg index 427ab827f4a1..ebb150c3cf76 100644 --- a/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg +++ b/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg @@ -1,7 +1,7 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +" id="m2dbe81a419" style="stroke:#008000;stroke-width:0.5;"/> - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + +" id="m954e0fa9c3" style="stroke:#008000;stroke-width:0.5;"/> - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - +" id="m368fc901b1" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="mc63e59a608" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -356,104 +356,104 @@ L 0 4 +" id="m556f96d829" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m27e32ca04a" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -477,8 +477,9 @@ z - - + + - + - - + + - + - + - + - + - - - - - - - - - - - +z +" id="DejaVuSans-100"/> + + + + + + + + + + @@ -633,13 +638,14 @@ L 316.669091 205.7097 - + - - + + - - - - +" id="DejaVuSans-88"/> + + + @@ -697,13 +702,13 @@ z - - + + - - + + @@ -718,22 +723,22 @@ L 391.117091 198.5097 - + - + - + - + @@ -745,15 +750,15 @@ L 405.517091 205.7097 - - + + - + diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 828cc1438be7..55df8c2067ad 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -3296,10 +3296,50 @@ def test_errorbar_offsets(fig_test, fig_ref): capsize=4, c=color) # Using manual errorbars - # n.b. errorbar draws the main plot at z=2.1 by default - ax_ref.plot(x, y, c=color, zorder=2.1) ax_ref.errorbar(x[shift::4], y[shift::4], yerr[shift::4], capsize=4, c=color, fmt='none') + ax_ref.plot(x, y, c=color) + + +@check_figures_equal() +def test_errorbar_default_order(fig_test, fig_ref): + # Now test default order of errorbars and plots. + x = list(range(5)) + y1 = np.full(5, -5) + y2 = np.full(5, 0) + y3 = np.full(5, 5) + + yerr = list(range(5)) + + ax_ref = fig_ref.subplots() + ax_ref.plot(x, y1, 'ro', markersize=60, zorder=2.12) + ax_ref.errorbar(x, y1, yerr=yerr, fmt='none', ecolor='k', zorder=2.13) + ax_ref.plot(x, y1, 'b.', markersize=20, zorder=2.15) + + ax_ref.plot(x, y2, 'ro', markersize=60, zorder=2.22) + ax_ref.errorbar(x, y2, yerr=yerr, fmt='none', ecolor='k', zorder=2.23) + ax_ref.plot(x, y2, 'gs', markersize=40, zorder=2.24) + ax_ref.plot(x, y2, 'b.', markersize=20, zorder=2.25) + + ax_ref.plot(x, y3, 'ro', markersize=60, zorder=2.32) + ax_ref.plot(x, y3, 'gs', markersize=40, zorder=2.33) + ax_ref.errorbar(x, y3, yerr=yerr, fmt='none', ecolor='k', zorder=2.34) + ax_ref.plot(x, y3, 'b.', markersize=20, zorder=2.35) + + ax_test = fig_test.subplots() + + ax_test.plot(x, y1, 'ro', markersize=60) + ax_test.errorbar(x, y1, yerr=yerr, fmt='none', ecolor='k') + ax_test.plot(x, y1, 'b.', markersize=20) + + ax_test.plot(x, y2, 'ro', markersize=60) + ax_test.errorbar(x, y2, yerr=yerr, fmt='gs', ecolor='k', markersize=40) + ax_test.plot(x, y2, 'b.', markersize=20) + + ax_test.plot(x, y3, 'ro', markersize=60) + ax_test.errorbar(x, y3, yerr=yerr, fmt='gs', ecolor='k', markersize=40, + barsabove=True) + ax_test.plot(x, y3, 'b.', markersize=20) @image_comparison(['hist_stacked_stepfilled', 'hist_stacked_stepfilled']) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index d66509b0ee78..f21088a50373 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -217,3 +217,36 @@ def test_marker_as_markerstyle(): assert_array_equal(line2.get_marker().vertices, triangle1.vertices) assert_array_equal(line3.get_marker().vertices, triangle1.vertices) + + +def test_lines_with_errorbars(): + x = list(range(5)) + y = np.zeros(5) + err = list(range(5)) + + fig = plt.figure() + ax = fig.add_subplot(111) + line, caplines, barcols = ax.errorbar(x, y, yerr=err, xerr=err) + + # check consistent data structure + assert len(caplines) == 4 + assert len(barcols) == 2 + ax_lines = ax.get_lines() + assert len(ax_lines) == 1 + assert line == ax_lines[0] + + # make sure line is still present + line.remove() + ax_lines = ax.get_lines() + assert len(ax_lines) == 1 + assert line == ax_lines[0] + + # remove elements + for cl in caplines: + cl.remove() + for bc in barcols: + bc.remove() + + # now the line should be gone too + ax_lines = ax.get_lines() + assert len(ax_lines) == 0 diff --git a/setup.cfg.template b/setup.cfg.template deleted file mode 100644 index 562544fb1e59..000000000000 --- a/setup.cfg.template +++ /dev/null @@ -1,37 +0,0 @@ -# Rename this file to setup.cfg to modify Matplotlib's build options. - -[egg_info] - -[libs] -# By default, Matplotlib downloads and builds its own copy of FreeType, and -# builds its own copy of Qhull. You may set the following to True to instead -# link against a system FreeType/Qhull. -#system_freetype = False -#system_qhull = False - -[packages] -# There are a number of data subpackages from Matplotlib that are -# considered optional. All except 'tests' data (meaning the baseline -# image files) are installed by default, but that can be changed here. -#tests = False -#sample_data = True - -[gui_support] -# Matplotlib supports multiple GUI toolkits, known as backends. -# The MacOSX backend requires the Cocoa headers included with XCode. -# You can select whether to build it by uncommenting the following line. -# It is never built on Linux or Windows, regardless of the config value. -# -#macosx = True - -[rc_options] -# User-configurable options -# -# Default backend, one of: Agg, Cairo, GTK3Agg, GTK3Cairo, MacOSX, Pdf, Ps, -# Qt4Agg, Qt5Agg, SVG, TkAgg, WX, WXAgg. -# -# The Agg, Ps, Pdf and SVG backends do not require external dependencies. Do -# not choose MacOSX if you have disabled the relevant extension modules. The -# default is determined by fallback. -# -#backend = Agg