[REF-2202] Implement event handlers for Plotly (#3397)

* pyi_generator: do not generate kwargs for event trigger props

event triggers are handled separately

* Implement event handlers for Plotly

* py38 compat: from __future__ import annotations
This commit is contained in:
Masen Furer 2024-05-31 13:04:19 -07:00 committed by GitHub
parent d9e718d7bd
commit 16fc3936a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 236 additions and 0 deletions

View File

@ -1,8 +1,11 @@
"""Component for displaying a plotly graph."""
from __future__ import annotations
from typing import Any, Dict, List
from reflex.base import Base
from reflex.components.component import NoSSRComponent
from reflex.event import EventHandler
from reflex.vars import Var
try:
@ -11,6 +14,76 @@ except ImportError:
Figure = Any # type: ignore
def _event_data_signature(e0: Var) -> List[Any]:
"""For plotly events with event data and no points.
Args:
e0: The event data.
Returns:
The event key extracted from the event data (if defined).
"""
return [Var.create_safe(f"{e0}?.event")]
def _event_points_data_signature(e0: Var) -> List[Any]:
"""For plotly events with event data containing a point array.
Args:
e0: The event data.
Returns:
The event data and the extracted points.
"""
return [
Var.create_safe(f"{e0}?.event"),
Var.create_safe(
f"extractPoints({e0}?.points)",
),
]
class _ButtonClickData(Base):
"""Event data structure for plotly UI buttons."""
menu: Any
button: Any
active: Any
def _button_click_signature(e0: _ButtonClickData) -> List[Any]:
"""For plotly button click events.
Args:
e0: The button click data.
Returns:
The menu, button, and active state.
"""
return [e0.menu, e0.button, e0.active]
def _passthrough_signature(e0: Var) -> List[Any]:
"""For plotly events with arbitrary serializable data, passed through directly.
Args:
e0: The event data.
Returns:
The event data.
"""
return [e0]
def _null_signature() -> List[Any]:
"""For plotly events with no data or non-serializable data. Nothing passed through.
Returns:
An empty list (nothing passed through).
"""
return []
class PlotlyLib(NoSSRComponent):
"""A component that wraps a plotly lib."""
@ -38,6 +111,105 @@ class Plotly(PlotlyLib):
# If true, the graph will resize when the window is resized.
use_resize_handler: Var[bool]
# Fired after the plot is redrawn.
on_after_plot: EventHandler[_passthrough_signature]
# Fired after the plot was animated.
on_animated: EventHandler[_null_signature]
# Fired while animating a single frame (does not currently pass data through).
on_animating_frame: EventHandler[_null_signature]
# Fired when an animation is interrupted (to start a new animation for example).
on_animation_interrupted: EventHandler[_null_signature]
# Fired when the plot is responsively sized.
on_autosize: EventHandler[_event_data_signature]
# Fired whenever mouse moves over a plot.
on_before_hover: EventHandler[_event_data_signature]
# Fired when a plotly UI button is clicked.
on_button_clicked: EventHandler[_button_click_signature]
# Fired when the plot is clicked.
on_click: EventHandler[_event_points_data_signature]
# Fired when a selection is cleared (via double click).
on_deselect: EventHandler[_null_signature]
# Fired when the plot is double clicked.
on_double_click: EventHandler[_passthrough_signature]
# Fired when a plot element is hovered over.
on_hover: EventHandler[_event_points_data_signature]
# Fired after the plot is layed out (zoom, pan, etc).
on_relayout: EventHandler[_passthrough_signature]
# Fired while the plot is being layed out.
on_relayouting: EventHandler[_passthrough_signature]
# Fired after the plot style is changed.
on_restyle: EventHandler[_passthrough_signature]
# Fired after the plot is redrawn.
on_redraw: EventHandler[_event_data_signature]
# Fired after selecting plot elements.
on_selected: EventHandler[_event_points_data_signature]
# Fired while dragging a selection.
on_selecting: EventHandler[_event_points_data_signature]
# Fired while an animation is occuring.
on_transitioning: EventHandler[_event_data_signature]
# Fired when a transition is stopped early.
on_transition_interrupted: EventHandler[_event_data_signature]
# Fired when a hovered element is no longer hovered.
on_unhover: EventHandler[_event_points_data_signature]
def add_custom_code(self) -> list[str]:
"""Add custom codes for processing the plotly points data.
Returns:
Custom code snippets for the module level.
"""
return [
"const removeUndefined = (obj) => {Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key]); return obj}",
"""
const extractPoints = (points) => {
if (!points) return [];
return points.map(point => {
const bbox = point.bbox ? removeUndefined({
x0: point.bbox.x0,
x1: point.bbox.x1,
y0: point.bbox.y0,
y1: point.bbox.y1,
z0: point.bbox.y0,
z1: point.bbox.y1,
}) : undefined;
return removeUndefined({
x: point.x,
y: point.y,
z: point.z,
lat: point.lat,
lon: point.lon,
curveNumber: point.curveNumber,
pointNumber: point.pointNumber,
pointNumbers: point.pointNumbers,
pointIndex: point.pointIndex,
'marker.color': point['marker.color'],
'marker.size': point['marker.size'],
bbox: bbox,
})
})
}
""",
]
def _render(self):
tag = super()._render()
figure = self.data.to(dict)

View File

@ -8,7 +8,9 @@ from reflex.vars import Var, BaseVar, ComputedVar
from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style
from typing import Any, Dict, List
from reflex.base import Base
from reflex.components.component import NoSSRComponent
from reflex.event import EventHandler
from reflex.vars import Var
try:
@ -16,6 +18,11 @@ try:
except ImportError:
Figure = Any # type: ignore
class _ButtonClickData(Base):
menu: Any
button: Any
active: Any
class PlotlyLib(NoSSRComponent):
@overload
@classmethod
@ -93,6 +100,7 @@ class PlotlyLib(NoSSRComponent):
...
class Plotly(PlotlyLib):
def add_custom_code(self) -> list[str]: ...
@overload
@classmethod
def create( # type: ignore
@ -108,21 +116,48 @@ class Plotly(PlotlyLib):
class_name: Optional[Any] = None,
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_after_plot: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_animated: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_animating_frame: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_animation_interrupted: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_autosize: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_before_hover: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_blur: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_button_clicked: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_click: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_context_menu: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_deselect: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_double_click: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_focus: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_hover: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mount: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
@ -147,9 +182,36 @@ class Plotly(PlotlyLib):
on_mouse_up: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_redraw: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_relayout: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_relayouting: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_restyle: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_scroll: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_selected: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_selecting: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_transition_interrupted: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_transitioning: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_unhover: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_unmount: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,

View File

@ -320,6 +320,7 @@ def _extract_class_props_as_ast_nodes(
all_props = []
kwargs = []
for target_class in clzs:
event_triggers = target_class().get_event_triggers()
# Import from the target class to ensure type hints are resolvable.
exec(f"from {target_class.__module__} import *", type_hint_globals)
for name, value in target_class.__annotations__.items():
@ -327,6 +328,7 @@ def _extract_class_props_as_ast_nodes(
name in spec.kwonlyargs
or name in EXCLUDED_PROPS
or name in all_props
or name in event_triggers
or (isinstance(value, str) and "ClassVar" in value)
):
continue