diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 84fc9d155b5f..6268273bcaf5 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -494,9 +494,27 @@ def do_3d_projection(self, renderer): ecs = (_zalpha(self._edgecolor3d, vzs) if self._depthshade else self._edgecolor3d) + + # Sort the points based on z coordinates + # Performance optimization: Create a sorted index array and reorder + # points and point properties according to the index array + z_markers_idx = np.argsort(vzs)[::-1] + + # Re-order items + vzs = vzs[z_markers_idx] + vxs = vxs[z_markers_idx] + vys = vys[z_markers_idx] + fcs = fcs[z_markers_idx] + ecs = ecs[z_markers_idx] + vps = np.column_stack((vxs, vys)) + + fcs = mcolors.to_rgba_array(fcs, self._alpha) ecs = mcolors.to_rgba_array(ecs, self._alpha) + self.set_edgecolors(ecs) - PathCollection.set_offsets(self, np.column_stack([vxs, vys])) + self.set_facecolors(fcs) + + PathCollection.set_offsets(self, vps) return np.min(vzs) if vzs.size else np.nan @@ -663,17 +681,16 @@ def do_3d_projection(self, renderer): in enumerate(zip(xyzlist, cface, cedge))), key=lambda x: x[0], reverse=True) - segments_2d = [s for z, s, fc, ec, idx in z_segments_2d] + zzs, segments_2d, self._facecolors2d, self._edgecolors2d, idxs = \ + zip(*z_segments_2d) + if self._codes3d is not None: - codes = [self._codes3d[idx] for z, s, fc, ec, idx in z_segments_2d] + codes = [self._codes3d[idx] for idx in idxs] PolyCollection.set_verts_and_codes(self, segments_2d, codes) else: PolyCollection.set_verts(self, segments_2d, self._closed) - self._facecolors2d = [fc for z, s, fc, ec, idx in z_segments_2d] - if len(self._edgecolors3d) == len(cface): - self._edgecolors2d = [ec for z, s, fc, ec, idx in z_segments_2d] - else: + if len(self._edgecolors3d) != len(cface): self._edgecolors2d = self._edgecolors3d # Return zorder value diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index 25bc6356a25e..dc441568cc5c 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -230,6 +230,51 @@ def test_scatter3d_color(): color='b', marker='s') +@pytest.mark.parametrize('azim', [-50, 130]) # yellow first, blue first +@check_figures_equal(extensions=['png']) +def test_marker_draw_order_data_reversed(fig_test, fig_ref, azim): + """ + Test that the draw order does not depend on the data point order. + + For the given viewing angle at azim=-50, the yellow marker should be in + front. For azim=130, the blue marker should be in front. + """ + x = [-1, 1] + y = [1, -1] + z = [0, 0] + color = ['b', 'y'] + ax = fig_test.add_subplot(projection='3d') + ax.scatter(x, y, z, s=3500, c=color) + ax.view_init(elev=0, azim=azim) + ax = fig_ref.add_subplot(projection='3d') + ax.scatter(x[::-1], y[::-1], z[::-1], s=3500, c=color[::-1]) + ax.view_init(elev=0, azim=azim) + + +@check_figures_equal(extensions=['png']) +def test_marker_draw_order_view_rotated(fig_test, fig_ref): + """ + Test that the draw order changes with the direction. + + If we rotate *azim* by 180 degrees and exchange the colors, the plot + plot should look the same again. + """ + azim = 130 + x = [-1, 1] + y = [1, -1] + z = [0, 0] + color = ['b', 'y'] + ax = fig_test.add_subplot(projection='3d') + # axis are not exactly invariant under 180 degree rotation -> deactivate + ax.set_axis_off() + ax.scatter(x, y, z, s=3500, c=color) + ax.view_init(elev=0, azim=azim) + ax = fig_ref.add_subplot(projection='3d') + ax.set_axis_off() + ax.scatter(x, y, z, s=3500, c=color[::-1]) # color reversed + ax.view_init(elev=0, azim=azim - 180) # view rotated by 180 degrees + + @mpl3d_image_comparison(['plot_3d_from_2d.png'], tol=0.01) def test_plot_3d_from_2d(): fig = plt.figure()