pythonmatplotliblegendmatplotlib-3dbar3d

Explain the error produced using plt.legend in a 3D stacked bar plot


enter image description here

The figure above was produced by this code

%matplotlib
import numpy as np
import matplotlib.pyplot as plt
x = y = np.array([1, 2])

fig = plt.figure(figsize=(5, 3))
ax1 = fig.add_subplot(111, projection='3d')
ax1.bar3d(x, y, [0,0], 0.5, 0.5, [1,1], shade=True, label='a')
ax1.bar3d(x, y, [1,1], 0.5, 0.5, [1,1], shade=True, label='b')
ax1.legend()

that asked also for a legend. As you can see, no legend but I've got this Traceback

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[1], line 12
     10 ax1.bar3d(x, y, [0,0], 0.5, 0.5, [1,1], shade=True, label='a')
     11 ax1.bar3d(x, y, [1,1], 0.5, 0.5, [1,1], shade=True, label='b')
---> 12 plt.legend()

File /usr/lib64/python3.11/site-packages/matplotlib/pyplot.py:2646, in legend(*args, **kwargs)
   2644 @_copy_docstring_and_deprecators(Axes.legend)
   2645 def legend(*args, **kwargs):
-> 2646     return gca().legend(*args, **kwargs)

File /usr/lib64/python3.11/site-packages/matplotlib/axes/_axes.py:313, in Axes.legend(self, *args, **kwargs)
    311 if len(extra_args):
    312     raise TypeError('legend only accepts two non-keyword arguments')
--> 313 self.legend_ = mlegend.Legend(self, handles, labels, **kwargs)
    314 self.legend_._remove_method = self._remove_legend
    315 return self.legend_

File /usr/lib64/python3.11/site-packages/matplotlib/_api/deprecation.py:454, in make_keyword_only.<locals>.wrapper(*args, **kwargs)
    448 if len(args) > name_idx:
    449     warn_deprecated(
    450         since, message="Passing the %(name)s %(obj_type)s "
    451         "positionally is deprecated since Matplotlib %(since)s; the "
    452         "parameter will become keyword-only %(removal)s.",
    453         name=name, obj_type=f"parameter of {func.__name__}()")
--> 454 return func(*args, **kwargs)

File /usr/lib64/python3.11/site-packages/matplotlib/legend.py:517, in Legend.__init__(self, parent, handles, labels, loc, numpoints, markerscale, markerfirst, scatterpoints, scatteryoffsets, prop, fontsize, labelcolor, borderpad, labelspacing, handlelength, handleheight, handletextpad, borderaxespad, columnspacing, ncols, mode, fancybox, shadow, title, title_fontsize, framealpha, edgecolor, facecolor, bbox_to_anchor, bbox_transform, frameon, handler_map, title_fontproperties, alignment, ncol)
    514 self._alignment = alignment
    516 # init with null renderer
--> 517 self._init_legend_box(handles, labels, markerfirst)
    519 tmp = self._loc_used_default
    520 self._set_loc(loc)

File /usr/lib64/python3.11/site-packages/matplotlib/legend.py:782, in Legend._init_legend_box(self, handles, labels, markerfirst)
    779         text_list.append(textbox._text)
    780         # Create the artist for the legend which represents the
    781         # original artist/handle.
--> 782         handle_list.append(handler.legend_artist(self, orig_handle,
    783                                                  fontsize, handlebox))
    784         handles_and_labels.append((handlebox, textbox))
    786 columnbox = []

File /usr/lib64/python3.11/site-packages/matplotlib/legend_handler.py:119, in HandlerBase.legend_artist(self, legend, orig_handle, fontsize, handlebox)
     95 """
     96 Return the artist that this HandlerBase generates for the given
     97 original artist/handle.
   (...)
    112 
    113 """
    114 xdescent, ydescent, width, height = self.adjust_drawing_area(
    115          legend, orig_handle,
    116          handlebox.xdescent, handlebox.ydescent,
    117          handlebox.width, handlebox.height,
    118          fontsize)
--> 119 artists = self.create_artists(legend, orig_handle,
    120                               xdescent, ydescent, width, height,
    121                               fontsize, handlebox.get_transform())
    123 if isinstance(artists, _Line2DHandleList):
    124     artists = [artists[0]]

File /usr/lib64/python3.11/site-packages/matplotlib/legend_handler.py:806, in HandlerPolyCollection.create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans)
    802 def create_artists(self, legend, orig_handle,
    803                    xdescent, ydescent, width, height, fontsize, trans):
    804     p = Rectangle(xy=(-xdescent, -ydescent),
    805                   width=width, height=height)
--> 806     self.update_prop(p, orig_handle, legend)
    807     p.set_transform(trans)
    808     return [p]

File /usr/lib64/python3.11/site-packages/matplotlib/legend_handler.py:78, in HandlerBase.update_prop(self, legend_handle, orig_handle, legend)
     76 def update_prop(self, legend_handle, orig_handle, legend):
---> 78     self._update_prop(legend_handle, orig_handle)
     80     legend._set_artist_props(legend_handle)
     81     legend_handle.set_clip_box(None)

File /usr/lib64/python3.11/site-packages/matplotlib/legend_handler.py:787, in HandlerPolyCollection._update_prop(self, legend_handle, orig_handle)
    783         return None
    785 # orig_handle is a PolyCollection and legend_handle is a Patch.
    786 # Directly set Patch color attributes (must be RGBA tuples).
--> 787 legend_handle._facecolor = first_color(orig_handle.get_facecolor())
    788 legend_handle._edgecolor = first_color(orig_handle.get_edgecolor())
    789 legend_handle._original_facecolor = orig_handle._original_facecolor

File /usr/lib64/python3.11/site-packages/matplotlib/legend_handler.py:775, in HandlerPolyCollection._update_prop.<locals>.first_color(colors)
    774 def first_color(colors):
--> 775     if colors.size == 0:
    776         return (0, 0, 0, 0)
    777     return tuple(colors[0])

AttributeError: 'tuple' object has no attribute 'size'

Can you help me at understanding what happened?


Solution

  • colors from bar3d

    legend_handle → Rectangle(xy=(-0, -0), width=20, height=7, angle=0)
    orig_handle → <mpl_toolkits.mplot3d.art3d.Poly3DCollection object at 0x00000253CF287AD0>
    
    # colors tuple of arrays, which causes AttributeError: 'tuple' object has no attribute 'size'
    colors =\
    (np.array([0.05065359, 0.19444443, 0.29411763, 1.]), np.array([0.06483662, 0.24888894, 0.37647067, 1.]), np.array([0.10738562, 0.41222224, 0.62352943, 1.]), np.array([0.05065359, 0.19444443, 0.29411763, 1.]), np.array([0.05065359, 0.19444443, 0.29411763, 1.]), np.array([0.0932026 , 0.35777772, 0.54117639, 1.]), np.array([0.06483662, 0.24888894, 0.37647067, 1.]), np.array([0.10738562, 0.41222224, 0.62352943, 1.]), np.array([0.10738562, 0.41222224, 0.62352943, 1.]), np.array([0.05065359, 0.19444443, 0.29411763, 1.]), np.array([0.0932026 , 0.35777772, 0.54117639, 1.]), np.array([0.10738562, 0.41222224, 0.62352943, 1.]))
    

    line 781 of legend_handler.py

    class HandlerPolyCollection(HandlerBase):
    
        def _update_prop(self, legend_handle, orig_handle):
            def first_color(colors):
                print(colors)  # added to check 
                colors = np.array(colors)  # added to fix
                if colors.size == 0:
                    return (0, 0, 0, 0)
                return tuple(colors[0])
    

    enter image description here


    How to create a legend for 3D bar seems like a safer option.

    x = y = np.array([1, 2])
    
    fig = plt.figure(figsize=(5, 3))
    ax1 = fig.add_subplot(111, projection='3d')
    ax1.bar3d(x, y, [0,0], 0.5, 0.5, [1,1], shade=True, label='a', color='tab:blue')
    blue_proxy = plt.Rectangle((0, 0), 1, 1, fc="tab:blue")
    
    ax1.bar3d(x, y, [1,1], 0.5, 0.5, [1,1], shade=True, label='b', color='tab:orange')
    orange_proxy = plt.Rectangle((0, 0), 1, 1, fc="tab:orange")
    
    ax1.legend([blue_proxy, orange_proxy], ['a', 'b'], bbox_to_anchor=(1.1, 0.5), loc='center left', frameon=False)
    

    enter image description here