Kaynağa Gözat

[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
Masen Furer 11 ay önce
ebeveyn
işleme
16fc3936a4

+ 172 - 0
reflex/components/plotly/plotly.py

@@ -1,8 +1,11 @@
 """Component for displaying a plotly graph."""
 """Component for displaying a plotly graph."""
+from __future__ import annotations
 
 
 from typing import Any, Dict, List
 from typing import Any, Dict, List
 
 
+from reflex.base import Base
 from reflex.components.component import NoSSRComponent
 from reflex.components.component import NoSSRComponent
+from reflex.event import EventHandler
 from reflex.vars import Var
 from reflex.vars import Var
 
 
 try:
 try:
@@ -11,6 +14,76 @@ except ImportError:
     Figure = Any  # type: ignore
     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):
 class PlotlyLib(NoSSRComponent):
     """A component that wraps a plotly lib."""
     """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.
     # If true, the graph will resize when the window is resized.
     use_resize_handler: Var[bool]
     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):
     def _render(self):
         tag = super()._render()
         tag = super()._render()
         figure = self.data.to(dict)
         figure = self.data.to(dict)

+ 62 - 0
reflex/components/plotly/plotly.pyi

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

+ 2 - 0
reflex/utils/pyi_generator.py

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