Skip to content

Commit 9ee03f7

Browse files
committed
Allow color array properties to be N-dimensional
This is needed to support the `table.cells.fill.color` and `table.cells.line.color` properties
1 parent 0fb8600 commit 9ee03f7

File tree

2 files changed

+78
-18
lines changed

2 files changed

+78
-18
lines changed

Diff for: _plotly_utils/basevalidators.py

+47-18
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ def is_homogeneous_array(v):
116116
(pd and isinstance(v, pd.Series)))
117117

118118

119+
def is_homogeneous_ndarray(v):
120+
"""
121+
Return whether a value is considered to be a homogeneous array
122+
"""
123+
return np and isinstance(v, np.ndarray)
124+
125+
119126
def is_simple_array(v):
120127
"""
121128
Return whether a value is considered to be an simple array
@@ -984,56 +991,78 @@ def description(self):
984991

985992
return valid_color_description
986993

987-
def validate_coerce(self, v):
994+
def validate_coerce(self, v, should_raise=True):
988995
if v is None:
989996
# Pass None through
990997
pass
991-
elif self.array_ok and is_homogeneous_array(v):
998+
elif self.array_ok and (
999+
is_homogeneous_array(v) or
1000+
is_homogeneous_ndarray(v)):
1001+
9921002
v_array = copy_to_readonly_numpy_array(v)
9931003
if (self.numbers_allowed() and
9941004
v_array.dtype.kind in ['u', 'i', 'f']):
9951005
# Numbers are allowed and we have an array of numbers.
9961006
# All good
9971007
v = v_array
9981008
else:
999-
validated_v = [self.vc_scalar(e) for e in v]
1009+
validated_v = [
1010+
self.validate_coerce(e, should_raise=False)
1011+
for e in v]
10001012

1001-
invalid_els = [
1002-
el for el, validated_el in zip(v, validated_v)
1003-
if validated_el is None
1004-
]
1005-
if invalid_els:
1013+
invalid_els = self.find_invalid_els(v, validated_v)
1014+
1015+
if invalid_els and should_raise:
10061016
self.raise_invalid_elements(invalid_els)
10071017

10081018
# ### Check that elements have valid colors types ###
1009-
if self.numbers_allowed():
1019+
elif self.numbers_allowed() or invalid_els:
10101020
v = copy_to_readonly_numpy_array(
10111021
validated_v, dtype='object')
10121022
else:
10131023
v = copy_to_readonly_numpy_array(
10141024
validated_v, dtype='unicode')
10151025
elif self.array_ok and is_simple_array(v):
1016-
validated_v = [self.vc_scalar(e) for e in v]
1026+
validated_v = [
1027+
self.validate_coerce(e, should_raise=False)
1028+
for e in v]
10171029

1018-
invalid_els = [
1019-
el for el, validated_el in zip(v, validated_v)
1020-
if validated_el is None
1021-
]
1030+
invalid_els = self.find_invalid_els(v, validated_v)
10221031

1023-
if invalid_els:
1032+
if invalid_els and should_raise:
10241033
self.raise_invalid_elements(invalid_els)
1025-
1026-
v = validated_v
1034+
else:
1035+
v = validated_v
10271036
else:
10281037
# Validate scalar color
10291038
validated_v = self.vc_scalar(v)
1030-
if validated_v is None:
1039+
if validated_v is None and should_raise:
10311040
self.raise_invalid_val(v)
10321041

10331042
v = validated_v
10341043

10351044
return v
10361045

1046+
def find_invalid_els(self, orig, validated, invalid_els=None):
1047+
"""
1048+
Helper method to find invalid elements in orig array.
1049+
Elements are invalid if their corresponding element in
1050+
the validated array is None.
1051+
1052+
This method handles deeply nested list structures
1053+
"""
1054+
if invalid_els is None:
1055+
invalid_els = []
1056+
1057+
for orig_el, validated_el in zip(orig, validated):
1058+
if is_array(orig_el):
1059+
self.find_invalid_els(orig_el, validated_el, invalid_els)
1060+
else:
1061+
if validated_el is None:
1062+
invalid_els.append(orig_el)
1063+
1064+
return invalid_els
1065+
10371066
def vc_scalar(self, v):
10381067
""" Helper to validate/coerce a scalar color """
10391068
return ColorValidator.perform_validate_coerce(

Diff for: _plotly_utils/tests/validators/test_color_validator.py

+31
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,23 @@ def test_acceptance_aok(val, validator_aok: ColorValidator):
105105
assert coerce_val == val
106106

107107

108+
@pytest.mark.parametrize('val', [
109+
'green',
110+
[['blue']],
111+
[['red', 'rgb(255, 0, 0)'], ['hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)']],
112+
np.array([['red', 'rgb(255, 0, 0)'], ['hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)']])
113+
])
114+
def test_acceptance_aok_2D(val, validator_aok: ColorValidator):
115+
coerce_val = validator_aok.validate_coerce(val)
116+
117+
if isinstance(val, np.ndarray):
118+
assert np.array_equal(coerce_val, val)
119+
elif isinstance(val, list):
120+
assert validator_aok.present(coerce_val) == tuple(val)
121+
else:
122+
assert coerce_val == val
123+
124+
108125
# ### Rejection ###
109126
@pytest.mark.parametrize('val',
110127
[[23], [0, 1, 2],
@@ -118,6 +135,20 @@ def test_rejection_aok(val, validator_aok: ColorValidator):
118135
assert 'Invalid element(s)' in str(validation_failure.value)
119136

120137

138+
@pytest.mark.parametrize('val',
139+
[[['redd', 'rgb(255, 0, 0)']],
140+
[['hsl(0, 100%, 50_00%)', 'hsla(0, 100%, 50%, 100%)'],
141+
['hsv(0, 100%, 100%)', 'purple']],
142+
[np.array(['hsl(0, 100%, 50_00%)', 'hsla(0, 100%, 50%, 100%)']),
143+
np.array(['hsv(0, 100%, 100%)', 'purple'])],
144+
[['blue'], [2]]])
145+
def test_rejection_aok_2D(val, validator_aok: ColorValidator):
146+
with pytest.raises(ValueError) as validation_failure:
147+
validator_aok.validate_coerce(val)
148+
149+
assert 'Invalid element(s)' in str(validation_failure.value)
150+
151+
121152
# Array ok, numbers ok
122153
# --------------------
123154
# ### Acceptance ###

0 commit comments

Comments
 (0)