Skip to content

Commit f8f0880

Browse files
make list-like hover_data more robust
1 parent 3adcfd2 commit f8f0880

File tree

3 files changed

+108
-19
lines changed

3 files changed

+108
-19
lines changed

packages/python/plotly/plotly/express/_core.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,11 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name):
10221022
args["hover_data"][k] = (True, args["hover_data"][k])
10231023
if not isinstance(args["hover_data"][k], tuple):
10241024
args["hover_data"][k] = (args["hover_data"][k], None)
1025+
if df_provided and args["hover_data"][k][1] is not None and k in df_input:
1026+
raise ValueError(
1027+
"Ambiguous input: values for '%s' appear both in hover_data and data_frame"
1028+
% k
1029+
)
10251030
# Loop over possible arguments
10261031
for field_name in all_attrables:
10271032
# Massaging variables
@@ -1074,19 +1079,36 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name):
10741079
and hover_data_is_dict
10751080
and args["hover_data"][str(argument)][1] is not None
10761081
):
1082+
# hover_data has onboard data
1083+
# previously-checked to have no name-conflict with data_frame
10771084
col_name = str(argument)
1078-
df_output[col_name] = args["hover_data"][col_name][1]
1079-
continue
1080-
1081-
if not df_provided:
1085+
real_argument = args["hover_data"][col_name][1]
1086+
1087+
if length and len(real_argument) != length:
1088+
raise ValueError(
1089+
"All arguments should have the same length. "
1090+
"The length of hover_data key `%s` is %d, whereas the "
1091+
"length of previously-processed arguments %s is %d"
1092+
% (
1093+
argument,
1094+
len(real_argument),
1095+
str(list(df_output.columns)),
1096+
length,
1097+
)
1098+
)
1099+
if hasattr(real_argument, "values"):
1100+
df_output[col_name] = real_argument.values
1101+
else:
1102+
df_output[col_name] = np.array(real_argument)
1103+
elif not df_provided:
10821104
raise ValueError(
10831105
"String or int arguments are only possible when a "
10841106
"DataFrame or an array is provided in the `data_frame` "
10851107
"argument. No DataFrame was provided, but argument "
10861108
"'%s' is of type str or int." % field
10871109
)
10881110
# Check validity of column name
1089-
if argument not in df_input.columns:
1111+
elif argument not in df_input.columns:
10901112
if wide_mode and argument in (value_name, var_name):
10911113
continue
10921114
else:
@@ -1098,20 +1120,21 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name):
10981120
if argument == "index":
10991121
err_msg += "\n To use the index, pass it in directly as `df.index`."
11001122
raise ValueError(err_msg)
1101-
if length and len(df_input[argument]) != length:
1123+
elif length and len(df_input[argument]) != length:
11021124
raise ValueError(
11031125
"All arguments should have the same length. "
11041126
"The length of column argument `df[%s]` is %d, whereas the "
1105-
"length of previous arguments %s is %d"
1127+
"length of previously-processed arguments %s is %d"
11061128
% (
11071129
field,
11081130
len(df_input[argument]),
11091131
str(list(df_output.columns)),
11101132
length,
11111133
)
11121134
)
1113-
col_name = str(argument)
1114-
df_output[col_name] = df_input[argument].values
1135+
else:
1136+
col_name = str(argument)
1137+
df_output[col_name] = df_input[argument].values
11151138
# ----------------- argument is a column / array / list.... -------
11161139
else:
11171140
if df_provided and hasattr(argument, "name"):
@@ -1137,7 +1160,7 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name):
11371160
raise ValueError(
11381161
"All arguments should have the same length. "
11391162
"The length of argument `%s` is %d, whereas the "
1140-
"length of previous arguments %s is %d"
1163+
"length of previously-processed arguments %s is %d"
11411164
% (field, len(argument), str(list(df_output.columns)), length)
11421165
)
11431166
if hasattr(argument, "values"):

packages/python/plotly/plotly/tests/test_core/test_px/test_px_hover.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import numpy as np
33
import pandas as pd
44
import pytest
5-
import plotly.graph_objects as go
65
from collections import OrderedDict # an OrderedDict is needed for Python 2
76

87

@@ -74,24 +73,69 @@ def test_newdatain_hover_data():
7473

7574

7675
def test_fail_wrong_column():
77-
with pytest.raises(ValueError):
78-
fig = px.scatter(
76+
with pytest.raises(ValueError) as err_msg:
77+
px.scatter(
7978
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
8079
x="a",
8180
y="b",
8281
hover_data={"d": True},
8382
)
84-
with pytest.raises(ValueError):
85-
fig = px.scatter(
83+
assert (
84+
"Value of 'hover_data_0' is not the name of a column in 'data_frame'."
85+
in str(err_msg.value)
86+
)
87+
with pytest.raises(ValueError) as err_msg:
88+
px.scatter(
8689
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
8790
x="a",
8891
y="b",
8992
hover_data={"d": ":.1f"},
9093
)
91-
with pytest.raises(ValueError):
92-
fig = px.scatter(
94+
assert (
95+
"Value of 'hover_data_0' is not the name of a column in 'data_frame'."
96+
in str(err_msg.value)
97+
)
98+
with pytest.raises(ValueError) as err_msg:
99+
px.scatter(
100+
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
101+
x="a",
102+
y="b",
103+
hover_data={"d": [3, 4, 5]}, # d is too long
104+
)
105+
assert (
106+
"All arguments should have the same length. The length of hover_data key `d` is 3"
107+
in str(err_msg.value)
108+
)
109+
with pytest.raises(ValueError) as err_msg:
110+
px.scatter(
111+
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
112+
x="a",
113+
y="b",
114+
hover_data={"d": (True, [3, 4, 5])}, # d is too long
115+
)
116+
assert (
117+
"All arguments should have the same length. The length of hover_data key `d` is 3"
118+
in str(err_msg.value)
119+
)
120+
with pytest.raises(ValueError) as err_msg:
121+
px.scatter(
122+
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
123+
x="a",
124+
y="b",
125+
hover_data={"c": [3, 4]},
126+
)
127+
assert (
128+
"Ambiguous input: values for 'c' appear both in hover_data and data_frame"
129+
in str(err_msg.value)
130+
)
131+
with pytest.raises(ValueError) as err_msg:
132+
px.scatter(
93133
{"a": [1, 2], "b": [3, 4], "c": [2, 1]},
94134
x="a",
95135
y="b",
96-
hover_data={"d": (True, [3, 4, 5])},
136+
hover_data={"c": (True, [3, 4])},
97137
)
138+
assert (
139+
"Ambiguous input: values for 'c' appear both in hover_data and data_frame"
140+
in str(err_msg.value)
141+
)

packages/python/plotly/plotly/tests/test_core/test_px/test_px_wide.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,8 @@ def append_special_case(df_in, args_in, args_expect, df_expect):
675675
dict(c=[7, 8, 7, 8], d=["a", "a", "b", "b"], value=[1, 2, 3, 4])
676676
),
677677
)
678-
# y = columns
678+
679+
# y = columns subset
679680
df = pd.DataFrame(dict(a=[1, 2], b=[3, 4]), index=[7, 8])
680681
df.index.name = "c"
681682
df.columns.name = "d"
@@ -686,6 +687,27 @@ def append_special_case(df_in, args_in, args_expect, df_expect):
686687
df_expect=pd.DataFrame(dict(c=[7, 8], variable=["a", "a"], value=[1, 2])),
687688
)
688689

690+
# list-like hover_data
691+
df = pd.DataFrame(dict(a=[1, 2], b=[3, 4]), index=[7, 8])
692+
df.index.name = "c"
693+
df.columns.name = "d"
694+
append_special_case(
695+
df_in=df,
696+
args_in=dict(x=None, y=None, color=None, hover_data=dict(new=[5, 6])),
697+
args_expect=dict(
698+
x="c",
699+
y="value",
700+
color="d",
701+
orientation="v",
702+
hover_data=dict(new=(True, [5, 6])),
703+
),
704+
df_expect=pd.DataFrame(
705+
dict(
706+
c=[7, 8, 7, 8], d=["a", "a", "b", "b"], new=[5, 6, 5, 6], value=[1, 2, 3, 4]
707+
)
708+
),
709+
)
710+
689711

690712
@pytest.mark.parametrize("df_in, args_in, args_expect, df_expect", special_cases)
691713
def test_wide_mode_internal_special_cases(df_in, args_in, args_expect, df_expect):

0 commit comments

Comments
 (0)