Skip to content

Commit 31179ae

Browse files
Merge pull request #2844 from plotly/figure-select-function-selector
Figure select function selector
2 parents 54efc64 + 9de2946 commit 31179ae

File tree

4 files changed

+155
-14
lines changed

4 files changed

+155
-14
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [4.12.0] - unreleased
66

7+
### Added
8+
9+
- For all `go.Figure` functions accepting a selector argument (e.g., `select_traces`), this argument can now also be a function which is passed each relevant graph object (in the case of `select_traces`, it is passed every trace in the figure). For graph objects where this function returns true, the graph object is included in the selection.
10+
711
### Updated
812

913
- Updated Plotly.js to version 1.57.0. See the [plotly.js CHANGELOG](https://github.com/plotly/plotly.js/blob/v1.57.0/CHANGELOG.md) for more information. These changes are reflected in the auto-generated `plotly.graph_objects` module.

Diff for: packages/python/plotly/plotly/basedatatypes.py

+23-14
Original file line numberDiff line numberDiff line change
@@ -813,24 +813,33 @@ def _perform_select_traces(self, filter_by_subplot, grid_subplot_refs, selector)
813813
def _selector_matches(obj, selector):
814814
if selector is None:
815815
return True
816+
# If selector is a dict, compare the fields
817+
if (type(selector) == type(dict())) or isinstance(selector, BasePlotlyType):
818+
# This returns True if selector is an empty dict
819+
for k in selector:
820+
if k not in obj:
821+
return False
816822

817-
for k in selector:
818-
if k not in obj:
819-
return False
820-
821-
obj_val = obj[k]
822-
selector_val = selector[k]
823-
824-
if isinstance(obj_val, BasePlotlyType):
825-
obj_val = obj_val.to_plotly_json()
823+
obj_val = obj[k]
824+
selector_val = selector[k]
826825

827-
if isinstance(selector_val, BasePlotlyType):
828-
selector_val = selector_val.to_plotly_json()
826+
if isinstance(obj_val, BasePlotlyType):
827+
obj_val = obj_val.to_plotly_json()
829828

830-
if obj_val != selector_val:
831-
return False
829+
if isinstance(selector_val, BasePlotlyType):
830+
selector_val = selector_val.to_plotly_json()
832831

833-
return True
832+
if obj_val != selector_val:
833+
return False
834+
return True
835+
# If selector is a function, call it with the obj as the argument
836+
elif type(selector) == type(lambda x: True):
837+
return selector(obj)
838+
else:
839+
raise TypeError(
840+
"selector must be dict or a function "
841+
"accepting a graph object returning a boolean."
842+
)
834843

835844
def for_each_trace(self, fn, selector=None, row=None, col=None, secondary_y=None):
836845
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import pytest
2+
3+
import plotly.graph_objects as go
4+
from plotly.basedatatypes import BaseFigure
5+
6+
7+
def test_selector_none():
8+
# should return True
9+
assert BaseFigure._selector_matches({}, None) == True # arbitrary,
10+
11+
12+
def test_selector_empty_dict():
13+
# should return True
14+
assert (
15+
BaseFigure._selector_matches(dict(hello="everybody"), {}) == True # arbitrary,
16+
)
17+
18+
19+
def test_selector_matches_subset_of_obj():
20+
# should return True
21+
assert (
22+
BaseFigure._selector_matches(
23+
dict(hello="everybody", today="cloudy", myiq=55),
24+
dict(myiq=55, today="cloudy"),
25+
)
26+
== True
27+
)
28+
29+
30+
def test_selector_has_nonmatching_key():
31+
# should return False
32+
assert (
33+
BaseFigure._selector_matches(
34+
dict(hello="everybody", today="cloudy", myiq=55),
35+
dict(myiq=55, cronenberg="scanners"),
36+
)
37+
== False
38+
)
39+
40+
41+
def test_selector_has_nonmatching_value():
42+
# should return False
43+
assert (
44+
BaseFigure._selector_matches(
45+
dict(hello="everybody", today="cloudy", myiq=55),
46+
dict(myiq=55, today="sunny"),
47+
)
48+
== False
49+
)
50+
51+
52+
def test_baseplotlytypes_could_match():
53+
# should return True
54+
obj = go.layout.Annotation(x=1, y=2, text="pat metheny")
55+
sel = go.layout.Annotation(x=1, y=2, text="pat metheny")
56+
assert BaseFigure._selector_matches(obj, sel) == True
57+
58+
59+
def test_baseplotlytypes_could_not_match():
60+
# should return False
61+
obj = go.layout.Annotation(x=1, y=3, text="pat metheny")
62+
sel = go.layout.Annotation(x=1, y=2, text="pat metheny")
63+
assert BaseFigure._selector_matches(obj, sel) == False
64+
65+
66+
def test_baseplotlytypes_cannot_match_subset():
67+
# should return False because "undefined" keys in sel return None, and are
68+
# compared (because "key in sel" returned True, it's value was None)
69+
obj = go.layout.Annotation(x=1, y=2, text="pat metheny")
70+
sel = go.layout.Annotation(x=1, y=2,)
71+
assert BaseFigure._selector_matches(obj, sel) == False
72+
73+
74+
def test_function_selector_could_match():
75+
# should return True
76+
obj = go.layout.Annotation(x=1, y=2, text="pat metheny")
77+
78+
def _sel(d):
79+
return d["x"] == 1 and d["y"] == 2 and d["text"] == "pat metheny"
80+
81+
assert BaseFigure._selector_matches(obj, _sel) == True
82+
83+
84+
def test_function_selector_could_not_match():
85+
# should return False
86+
obj = go.layout.Annotation(x=1, y=2, text="pat metheny")
87+
88+
def _sel(d):
89+
return d["x"] == 1 and d["y"] == 3 and d["text"] == "pat metheny"
90+
91+
assert BaseFigure._selector_matches(obj, _sel) == False

Diff for: packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_traces.py

+37
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,43 @@ def test_select_property_and_grid(self):
225225
# Valid row/col and valid selector but the intersection is empty
226226
self.assert_select_traces([], selector={"type": "markers"}, row=3, col=1)
227227

228+
def test_select_with_function(self):
229+
def _check_trace_key(k, v):
230+
def f(t):
231+
try:
232+
return t[k] == v
233+
except LookupError:
234+
return False
235+
236+
return f
237+
238+
# (1, 1)
239+
self.assert_select_traces(
240+
[0], selector=_check_trace_key("mode", "markers"), row=1, col=1
241+
)
242+
self.assert_select_traces(
243+
[1], selector=_check_trace_key("type", "bar"), row=1, col=1
244+
)
245+
246+
# (2, 1)
247+
self.assert_select_traces(
248+
[2, 9], selector=_check_trace_key("mode", "lines"), row=2, col=1
249+
)
250+
251+
# (1, 2)
252+
self.assert_select_traces(
253+
[4], selector=_check_trace_key("marker.color", "green"), row=1, col=2
254+
)
255+
256+
# Valid row/col and valid selector but the intersection is empty
257+
self.assert_select_traces(
258+
[], selector=_check_trace_key("type", "markers"), row=3, col=1
259+
)
260+
261+
def test_select_traces_type_error(self):
262+
with self.assertRaises(TypeError):
263+
self.assert_select_traces([0], selector=123, row=1, col=1)
264+
228265
def test_for_each_trace_lowercase_names(self):
229266
# Names are all uppercase to start
230267
original_names = [t.name for t in self.fig.data]

0 commit comments

Comments
 (0)