Skip to content

Commit

Permalink
[REF-2202] Implement event handlers for Plotly (#3397)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
masenf authored May 31, 2024
1 parent d9e718d commit 16fc393
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 0 deletions.
172 changes: 172 additions & 0 deletions reflex/components/plotly/plotly.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -11,6 +14,76 @@
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."""

Expand Down Expand Up @@ -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)
Expand Down
62 changes: 62 additions & 0 deletions reflex/components/plotly/plotly.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ 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:
from plotly.graph_objects import Figure # type: ignore
except ImportError:
Figure = Any # type: ignore

class _ButtonClickData(Base):
menu: Any
button: Any
active: Any

class PlotlyLib(NoSSRComponent):
@overload
@classmethod
Expand Down Expand Up @@ -93,6 +100,7 @@ class PlotlyLib(NoSSRComponent):
...

class Plotly(PlotlyLib):
def add_custom_code(self) -> list[str]: ...
@overload
@classmethod
def create( # type: ignore
Expand All @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions reflex/utils/pyi_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,15 @@ 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():
if (
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
Expand Down

0 comments on commit 16fc393

Please sign in to comment.