plotly.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. """Component for displaying a plotly graph."""
  2. from __future__ import annotations
  3. from typing import Any, Dict, List
  4. from reflex.base import Base
  5. from reflex.components.component import Component, NoSSRComponent
  6. from reflex.components.core.cond import color_mode_cond
  7. from reflex.event import EventHandler
  8. from reflex.utils import console
  9. from reflex.vars import Var
  10. try:
  11. from plotly.graph_objects import Figure, layout
  12. Template = layout.Template
  13. except ImportError:
  14. console.warn("Plotly is not installed. Please run `pip install plotly`.")
  15. Figure = Any # type: ignore
  16. Template = Any # type: ignore
  17. def _event_data_signature(e0: Var) -> List[Any]:
  18. """For plotly events with event data and no points.
  19. Args:
  20. e0: The event data.
  21. Returns:
  22. The event key extracted from the event data (if defined).
  23. """
  24. return [Var.create_safe(f"{e0}?.event", _var_is_string=False)]
  25. def _event_points_data_signature(e0: Var) -> List[Any]:
  26. """For plotly events with event data containing a point array.
  27. Args:
  28. e0: The event data.
  29. Returns:
  30. The event data and the extracted points.
  31. """
  32. return [
  33. Var.create_safe(f"{e0}?.event", _var_is_string=False),
  34. Var.create_safe(
  35. f"extractPoints({e0}?.points)",
  36. _var_is_string=False,
  37. ),
  38. ]
  39. class _ButtonClickData(Base):
  40. """Event data structure for plotly UI buttons."""
  41. menu: Any
  42. button: Any
  43. active: Any
  44. def _button_click_signature(e0: _ButtonClickData) -> List[Any]:
  45. """For plotly button click events.
  46. Args:
  47. e0: The button click data.
  48. Returns:
  49. The menu, button, and active state.
  50. """
  51. return [e0.menu, e0.button, e0.active]
  52. def _passthrough_signature(e0: Var) -> List[Any]:
  53. """For plotly events with arbitrary serializable data, passed through directly.
  54. Args:
  55. e0: The event data.
  56. Returns:
  57. The event data.
  58. """
  59. return [e0]
  60. def _null_signature() -> List[Any]:
  61. """For plotly events with no data or non-serializable data. Nothing passed through.
  62. Returns:
  63. An empty list (nothing passed through).
  64. """
  65. return []
  66. class Plotly(NoSSRComponent):
  67. """Display a plotly graph."""
  68. library = "react-plotly.js@2.6.0"
  69. lib_dependencies: List[str] = ["plotly.js@2.22.0"]
  70. tag = "Plot"
  71. is_default = True
  72. # The figure to display. This can be a plotly figure or a plotly data json.
  73. data: Var[Figure]
  74. # The layout of the graph.
  75. layout: Var[Dict]
  76. # The template for visual appearance of the graph.
  77. template: Var[Template]
  78. # The config of the graph.
  79. config: Var[Dict]
  80. # If true, the graph will resize when the window is resized.
  81. use_resize_handler: Var[bool] = Var.create_safe(True)
  82. # Fired after the plot is redrawn.
  83. on_after_plot: EventHandler[_passthrough_signature]
  84. # Fired after the plot was animated.
  85. on_animated: EventHandler[_null_signature]
  86. # Fired while animating a single frame (does not currently pass data through).
  87. on_animating_frame: EventHandler[_null_signature]
  88. # Fired when an animation is interrupted (to start a new animation for example).
  89. on_animation_interrupted: EventHandler[_null_signature]
  90. # Fired when the plot is responsively sized.
  91. on_autosize: EventHandler[_event_data_signature]
  92. # Fired whenever mouse moves over a plot.
  93. on_before_hover: EventHandler[_event_data_signature]
  94. # Fired when a plotly UI button is clicked.
  95. on_button_clicked: EventHandler[_button_click_signature]
  96. # Fired when the plot is clicked.
  97. on_click: EventHandler[_event_points_data_signature]
  98. # Fired when a selection is cleared (via double click).
  99. on_deselect: EventHandler[_null_signature]
  100. # Fired when the plot is double clicked.
  101. on_double_click: EventHandler[_passthrough_signature]
  102. # Fired when a plot element is hovered over.
  103. on_hover: EventHandler[_event_points_data_signature]
  104. # Fired after the plot is layed out (zoom, pan, etc).
  105. on_relayout: EventHandler[_passthrough_signature]
  106. # Fired while the plot is being layed out.
  107. on_relayouting: EventHandler[_passthrough_signature]
  108. # Fired after the plot style is changed.
  109. on_restyle: EventHandler[_passthrough_signature]
  110. # Fired after the plot is redrawn.
  111. on_redraw: EventHandler[_event_data_signature]
  112. # Fired after selecting plot elements.
  113. on_selected: EventHandler[_event_points_data_signature]
  114. # Fired while dragging a selection.
  115. on_selecting: EventHandler[_event_points_data_signature]
  116. # Fired while an animation is occuring.
  117. on_transitioning: EventHandler[_event_data_signature]
  118. # Fired when a transition is stopped early.
  119. on_transition_interrupted: EventHandler[_event_data_signature]
  120. # Fired when a hovered element is no longer hovered.
  121. on_unhover: EventHandler[_event_points_data_signature]
  122. def add_imports(self) -> dict[str, str]:
  123. """Add imports for the plotly component.
  124. Returns:
  125. The imports for the plotly component.
  126. """
  127. return {
  128. # For merging plotly data/layout/templates.
  129. "mergician@v2.0.2": "mergician"
  130. }
  131. def add_custom_code(self) -> list[str]:
  132. """Add custom codes for processing the plotly points data.
  133. Returns:
  134. Custom code snippets for the module level.
  135. """
  136. return [
  137. "const removeUndefined = (obj) => {Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key]); return obj}",
  138. """
  139. const extractPoints = (points) => {
  140. if (!points) return [];
  141. return points.map(point => {
  142. const bbox = point.bbox ? removeUndefined({
  143. x0: point.bbox.x0,
  144. x1: point.bbox.x1,
  145. y0: point.bbox.y0,
  146. y1: point.bbox.y1,
  147. z0: point.bbox.y0,
  148. z1: point.bbox.y1,
  149. }) : undefined;
  150. return removeUndefined({
  151. x: point.x,
  152. y: point.y,
  153. z: point.z,
  154. lat: point.lat,
  155. lon: point.lon,
  156. curveNumber: point.curveNumber,
  157. pointNumber: point.pointNumber,
  158. pointNumbers: point.pointNumbers,
  159. pointIndex: point.pointIndex,
  160. 'marker.color': point['marker.color'],
  161. 'marker.size': point['marker.size'],
  162. bbox: bbox,
  163. })
  164. })
  165. }
  166. """,
  167. ]
  168. @classmethod
  169. def create(cls, *children, **props) -> Component:
  170. """Create the Plotly component.
  171. Args:
  172. *children: The children of the component.
  173. **props: The properties of the component.
  174. Returns:
  175. The Plotly component.
  176. """
  177. from plotly.io import templates
  178. responsive_template = color_mode_cond(
  179. light=Var.create_safe(templates["plotly"]).to(dict),
  180. dark=Var.create_safe(templates["plotly_dark"]).to(dict),
  181. )
  182. if isinstance(responsive_template, Var):
  183. # Mark the conditional Var as a Template to avoid type mismatch
  184. responsive_template = responsive_template.to(Template)
  185. props.setdefault("template", responsive_template)
  186. return super().create(*children, **props)
  187. def _exclude_props(self) -> set[str]:
  188. # These props are handled specially in the _render function
  189. return {"data", "layout", "template"}
  190. def _render(self):
  191. tag = super()._render()
  192. figure = self.data.to(dict)
  193. merge_dicts = [] # Data will be merged and spread from these dict Vars
  194. if self.layout is not None:
  195. # Why is this not a literal dict? Great question... it didn't work
  196. # reliably because of how _var_name_unwrapped strips the outer curly
  197. # brackets if any of the contained Vars depend on state.
  198. layout_dict = Var.create_safe(
  199. f"{{'layout': {self.layout.to(dict)._var_name_unwrapped}}}"
  200. ).to(dict)
  201. merge_dicts.append(layout_dict)
  202. if self.template is not None:
  203. template_dict = Var.create_safe(
  204. {"layout": {"template": self.template.to(dict)}}
  205. )
  206. template_dict._var_data = None # To avoid stripping outer curly brackets
  207. merge_dicts.append(template_dict)
  208. if merge_dicts:
  209. tag.special_props.add(
  210. # Merge all dictionaries and spread the result over props.
  211. Var.create_safe(
  212. f"{{...mergician({figure._var_name_unwrapped},"
  213. f"{','.join(md._var_name_unwrapped for md in merge_dicts)})}}",
  214. _var_is_string=False,
  215. ),
  216. )
  217. else:
  218. # Spread the figure dict over props, nothing to merge.
  219. tag.special_props.add(
  220. Var.create_safe(
  221. f"{{...{figure._var_name_unwrapped}}}", _var_is_string=False
  222. )
  223. )
  224. return tag