diff --git a/CHANGELOG.md b/CHANGELOG.md index 1632f92203..7c471ae6d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura for the contribution! - Fix `plotly.io.write_images` ignoring `layout.width`/`layout.height` (and the template's) and exporting every figure at the default 700x500 size; it now uses the same width/height priority as `write_image` and `to_image` [[#5784](https://github.com/plotly/plotly.py/pull/5784)], with thanks to @breken-ai for the contribution! - Fix color utilities mutating the list of colors passed to them [[#5836](https://github.com/plotly/plotly.py/pull/5836)], with thanks to @Enesuygurs for the contribution! +- Fix `graph_objects` ignoring the mask of numpy masked arrays passed to data array properties such as `x` and `y` [[#5861](https://github.com/plotly/plotly.py/pull/5861)], with thanks to @LiShuochen for the contribution! ## [7.1.0] - 2026-09-15 diff --git a/_plotly_utils/basevalidators.py b/_plotly_utils/basevalidators.py index 1ffc63e5ad..c3a577904f 100644 --- a/_plotly_utils/basevalidators.py +++ b/_plotly_utils/basevalidators.py @@ -417,6 +417,17 @@ def validate_coerce(self, v): if is_none_or_typed_array_spec(v): pass elif is_homogeneous_array(v): + ma = get_module("numpy.ma", should_load=False) + if ma and ma.isMaskedArray(v) and v.dtype.kind in "biufU" and v.mask.any(): + np = get_module("numpy") + # Replace masked values with NaN (numeric arrays) or None (boolean + # and string arrays) so that they are treated as missing values + if v.dtype.kind in "iu": + v = v.astype("float64") + if v.dtype.kind == "f": + v = v.filled(np.nan) + else: + v = np.where(v.mask, None, v.data.astype("object")) v = copy_to_readonly_numpy_array(v) elif is_simple_array(v): v = to_scalar_or_list(v) diff --git a/tests/test_plotly_utils/validators/test_dataarray_validator.py b/tests/test_plotly_utils/validators/test_dataarray_validator.py index 50a601dc1b..7f9b052704 100644 --- a/tests/test_plotly_utils/validators/test_dataarray_validator.py +++ b/tests/test_plotly_utils/validators/test_dataarray_validator.py @@ -42,6 +42,37 @@ def test_validator_acceptance_homogeneous(val, validator): assert np.array_equal(validator.present(coerce_val), val) +@pytest.mark.parametrize( + "val,expected", + [ + (np.ma.array([1, 2, 3], mask=[0, 1, 0]), np.array([1, np.nan, 3])), + ( + np.ma.array([1, 2, 3], mask=[0, 1, 0], dtype="float32"), + np.array([1, np.nan, 3], dtype="float32"), + ), + (np.ma.array([1, 2, 3], mask=True), np.array([np.nan, np.nan, np.nan])), + (np.ma.array([1, 2, 3], mask=False), np.array([1, 2, 3])), + ( + np.ma.array([[1, 2], [3, 4]], mask=[[0, 1], [0, 0]]), + np.array([[1, np.nan], [3, 4]]), + ), + ( + np.ma.array(["a", "b", "c"], mask=[0, 1, 0]), + np.array(["a", None, "c"], dtype="object"), + ), + ( + np.ma.array([True, False, True], mask=[0, 1, 0]), + np.array([True, None, True], dtype="object"), + ), + ], +) +def test_validator_acceptance_masked(val, expected, validator): + coerce_val = validator.validate_coerce(val) + assert not np.ma.isMaskedArray(coerce_val) + assert coerce_val.dtype == expected.dtype + np.testing.assert_array_equal(coerce_val, expected) + + # Rejection @pytest.mark.parametrize("val", ["Hello", 23, set(), {}]) def test_rejection(val, validator):