[Bug]: Line3D.draw requires .shape on _verts3d, but set_data_3d accepts and stores any array-like · Issue #32127 · matplotlib/matplotlib · GitHub
Skip to content

[Bug]: Line3D.draw requires .shape on _verts3d, but set_data_3d accepts and stores any array-like #32127

Description

@thc1006

Bug summary

Line3D.set_data_3d documents its parameters as array-like and accepts anything iterable, storing the arguments verbatim in _verts3d. Line3D.draw then reads self._verts3d[0].shape, so a list, which is array-like and passes the setter's own check, raises AttributeError during draw.

The .shape access lives inside if np.any(scale_mask):, so it is only reached when a coordinate is invalid for the current axis scale. A finite list draws fine, which is why this can sit unnoticed for a long time and then surface the first time a NaN appears.

This is new in 3.11. 3.10.9 has no such branch and draws the same input.

Code for reproduction

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(projection="3d")
(line,) = ax.plot([0.0], [0.0], [0.0])

line.set_data_3d([float("nan")], [float("nan")], [float("nan")])
fig.canvas.draw()

Actual outcome

Traceback (most recent call last):
  ...
  File ".../mpl_toolkits/mplot3d/art3d.py", line 363, in draw
    (len(self._verts3d), *self._verts3d[0].shape)
                          ^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'list' object has no attribute 'shape'

Expected outcome

The figure draws, as it does on 3.10.9 and as it does on 3.11.1 when the same values are passed as arrays:

import numpy as np
line.set_data_3d(np.array([np.nan]), np.array([np.nan]), np.array([np.nan]))
fig.canvas.draw()   # fine

Additional information

The two sides disagree about what _verts3d holds.

set_data_3d checks only that each argument is iterable and stores them as given:

for name, xyz in zip('xyz', args):
    if not np.iterable(xyz):
        raise RuntimeError(f'{name} must be a sequence')
self._verts3d = args

draw requires them to have .shape:

if np.any(scale_mask):
    mask = np.broadcast_to(
        scale_mask,
        (len(self._verts3d), *self._verts3d[0].shape)
    )

Behaviour by input and version:

coordinates _verts3d holds 3.10.9 3.11.1
finite lists draws draws
NaN lists draws AttributeError
NaN arrays draws draws

The narrowest fix is np.shape(self._verts3d[0]) in draw, which leaves what get_data_3d() returns unchanged. Converting in the setter also works but changes the stored type, so a caller that reads back what it passed would see something different. I have both verified locally and am happy to open a PR for the first.

Worth noting because it is how this was found: #22308 recommended set_data([a], [b]) followed by set_3d_properties([c]) as the way to update a Line3D, which produces exactly this state. Code written against that advice draws on 3.10 and raises on 3.11 as soon as a coordinate goes non-finite.

Operating system

Linux (Debian 13, kernel 6.12)

Matplotlib Version

3.11.1 (3.10.9 unaffected; the branch is present on main)

Matplotlib Backend

Agg

Python version

3.14

Jupyter version

N/A

Installation

pip

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions