-
-
Notifications
You must be signed in to change notification settings - Fork 822
feat: Extend .interactive()
with tooltip and legend
#3394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1e85a59
e76a3e2
7c127fd
d20ecc8
7fea185
343bb94
0b5b688
cbf51c4
29994be
8d85e02
f3a5a17
f2305bd
2981860
32f900e
cc3df47
706f374
3eb743e
4794028
a8001da
49bd736
b125c19
01644a2
24b8aab
708457c
51fd920
885767a
728d3cb
61281d7
5204759
ab7dab4
c6dac9d
5f47fa0
bd7ccfb
4b12df6
d9f8850
479fe27
3319bab
55d79ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4070,33 +4070,117 @@ def add_selection(self, *params) -> Self: # noqa: ANN002 | |
return self.add_params(*params) | ||
|
||
def interactive( | ||
self, name: str | None = None, bind_x: bool = True, bind_y: bool = True | ||
) -> Self: | ||
self, | ||
name: str | None = None, | ||
bind_x: bool = True, | ||
bind_y: bool = True, | ||
tooltip: bool = True, | ||
legend: bool | LegendChannel_T = False, | ||
dangotbanned marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) -> Chart: | ||
dangotbanned marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Make chart axes scales interactive. | ||
Add common interactive elements to the chart. | ||
|
||
Parameters | ||
---------- | ||
name : string | ||
name : string or None | ||
The parameter name to use for the axes scales. This name should be | ||
unique among all parameters within the chart. | ||
unique among all parameters within the chart | ||
bind_x : boolean, default True | ||
If true, then bind the interactive scales to the x-axis | ||
Bind the interactive scales to the x-axis | ||
bind_y : boolean, default True | ||
If true, then bind the interactive scales to the y-axis | ||
Bind the interactive scales to the y-axis | ||
tooltip : boolean, default True, | ||
Add a tooltip containing the encodings used in the chart | ||
legend : boolean or string, default True | ||
A single encoding channel to be used to create a clickable legend. | ||
The deafult is to guess from the spec based on the most commonly used legend encodings. | ||
|
||
Returns | ||
------- | ||
chart : | ||
copy of self, with interactive axes added | ||
copy of self, with interactivity added | ||
|
||
""" | ||
encodings: list[SingleDefUnitChannel_T] = [] | ||
if bind_x: | ||
encodings.append("x") | ||
if bind_y: | ||
encodings.append("y") | ||
return self.add_params(selection_interval(bind="scales", encodings=encodings)) | ||
chart: Chart = self.copy().add_params( | ||
dangotbanned marked this conversation as resolved.
Show resolved
Hide resolved
|
||
selection_interval(bind="scales", encodings=encodings) | ||
) | ||
# We can't simply use configure_mark since configure methods | ||
# are not allowed in layered specs | ||
if tooltip: | ||
chart = _add_tooltip(chart) | ||
legend_encodings_missing = utils.is_undefined(chart.encoding) | ||
if legend and not legend_encodings_missing: | ||
facet_encoding: FacetedEncoding = chart.encoding | ||
if not isinstance(legend, str): | ||
legend = _infer_legend_encoding(facet_encoding) | ||
|
||
facet_legend = facet_encoding[legend] | ||
legend_type = facet_legend["type"] | ||
if utils.is_undefined(legend_type): | ||
legend_type = facet_legend.to_dict(context={"data": chart.data})["type"] | ||
|
||
if legend_type == "nominal": | ||
# TODO Ideally this would work for ordinal data too | ||
legend_selection = selection_point(bind="legend", encodings=[legend]) | ||
initial_computed_domain = param(expr=f"domain('{legend}')") | ||
nonreactive_domain = param( | ||
react=False, expr=initial_computed_domain.name | ||
) | ||
scale = facet_legend["scale"] | ||
if utils.is_undefined(scale): | ||
scale = {"domain": nonreactive_domain} | ||
else: | ||
scale["domain"] = nonreactive_domain | ||
chart = chart.add_params( | ||
legend_selection, | ||
initial_computed_domain, | ||
nonreactive_domain, | ||
).transform_filter(legend_selection) | ||
else: | ||
msg = f"Expected only 'nominal' legend type but got {legend_type!r}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same reasoning as for EditNot exactly the same reasoning, as I think cases other than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that sounds good. I think the only other type we will support it |
||
raise NotImplementedError(msg) | ||
return chart | ||
|
||
|
||
LegendChannel_T: TypeAlias = Literal[ | ||
dangotbanned marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"color", | ||
"fill", | ||
"shape", | ||
"stroke", | ||
"opacity", | ||
"fillOpacity", | ||
"strokeOpacity", | ||
"strokeWidth", | ||
"strokeDash", | ||
"angle", # TODO Untested | ||
"radius", # TODO Untested | ||
"radius2", # TODO Untested | ||
# "size", # TODO Currently size is not working, renders empty legend | ||
] | ||
|
||
|
||
def _add_tooltip(chart: _TChart, /) -> _TChart: | ||
joelostblom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if isinstance(chart.mark, str): | ||
chart.mark = {"type": chart.mark, "tooltip": True} | ||
dangotbanned marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else: | ||
chart.mark.tooltip = True | ||
return chart | ||
|
||
|
||
def _infer_legend_encoding(encoding: FacetedEncoding, /) -> LegendChannel_T: | ||
"""Set the legend to commonly used encodings by default.""" | ||
_channels = t.get_args(LegendChannel_T) | ||
it = (ch for ch in _channels if not utils.is_undefined(encoding[ch])) | ||
if legend := next(it, None): | ||
return legend | ||
else: | ||
msg = f"Unable to infer target channel for 'legend'.\n\n{encoding!r}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It wasn't clear to me what the intention was for this case. At a minimum I think there should be a warning displayed, rather than silently continuing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what is the best approach here. I agree that a warning/error is more explicit than silently continuing, but since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I do see the argument for low-friction. The issue is the cost the solution has. We have useful info in Screenshot from discussionWithout providing feedback like this, I'd assume we'd need to write the equivalent of that into the docstring anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good point, and it brings up an interesting distinction I didn't think of before. Whereas setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's fair!
My bad for writing the last comment without testing. I guess the starting point would be the same though, encountering that error currently would require |
||
raise NotImplementedError(msg) | ||
|
||
|
||
def _check_if_valid_subspec( | ||
|
@@ -5176,6 +5260,16 @@ def sphere() -> SphereGenerator: | |
return core.SphereGenerator(sphere=True) | ||
|
||
|
||
_TChart = TypeVar( | ||
"_TChart", | ||
Chart, | ||
RepeatChart, | ||
ConcatChart, | ||
HConcatChart, | ||
VConcatChart, | ||
FacetChart, | ||
LayerChart, | ||
) | ||
ChartType: TypeAlias = Union[ | ||
Chart, RepeatChart, ConcatChart, HConcatChart, VConcatChart, FacetChart, LayerChart | ||
] | ||
|
Uh oh!
There was an error while loading. Please reload this page.