Skip to content

Fix errobar order #17231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
2 changes: 1 addition & 1 deletion doc/api/lines_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Classes

Line2D
VertexSelector
Line2DWithErrorbars

Functions
---------
Expand All @@ -26,4 +27,3 @@ Functions
:template: autosummary.rst

segment_hits

174 changes: 133 additions & 41 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
80 changes: 80 additions & 0 deletions lib/matplotlib/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Binary file modified lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf
Binary file not shown.
Binary file modified lib/matplotlib/tests/baseline_images/test_legend/fancy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading