瀏覽代碼

[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 月之前
父節點
當前提交
16fc3936a4
共有 3 個文件被更改,包括 236 次插入0 次删除
  1. 172 0
      reflex/components/plotly/plotly.py
  2. 62 0
      reflex/components/plotly/plotly.pyi
  3. 2 0
      reflex/utils/pyi_generator.py

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

@@ -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)

+ 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.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,

+ 2 - 0
reflex/utils/pyi_generator.py

@@ -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