plotly.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. """Component for displaying a plotly graph."""
  2. from __future__ import annotations
  3. from typing import Any, Dict, List, Tuple, Union
  4. from typing_extensions import TypedDict, TypeVar
  5. from reflex.components.component import Component, NoSSRComponent
  6. from reflex.components.core.cond import color_mode_cond
  7. from reflex.event import EventHandler, no_args_event_spec
  8. from reflex.utils import console
  9. from reflex.utils.imports import ImportDict, ImportVar
  10. from reflex.vars.base import LiteralVar, Var
  11. try:
  12. from plotly.graph_objects import Figure, layout
  13. Template = layout.Template
  14. except ImportError:
  15. console.warn("Plotly is not installed. Please run `pip install plotly`.")
  16. Figure = Any
  17. Template = Any
  18. def _event_points_data_signature(e0: Var) -> Tuple[Var[List[Point]]]:
  19. """For plotly events with event data containing a point array.
  20. Args:
  21. e0: The event data.
  22. Returns:
  23. The event data and the extracted points.
  24. """
  25. return (Var(_js_expr=f"extractPoints({e0}?.points)"),)
  26. T = TypeVar("T")
  27. ItemOrList = Union[T, List[T]]
  28. class BBox(TypedDict):
  29. """Bounding box for a point in a plotly graph."""
  30. x0: Union[float, int, None]
  31. x1: Union[float, int, None]
  32. y0: Union[float, int, None]
  33. y1: Union[float, int, None]
  34. z0: Union[float, int, None]
  35. z1: Union[float, int, None]
  36. class Point(TypedDict):
  37. """A point in a plotly graph."""
  38. x: Union[float, int, None]
  39. y: Union[float, int, None]
  40. z: Union[float, int, None]
  41. lat: Union[float, int, None]
  42. lon: Union[float, int, None]
  43. curveNumber: Union[int, None]
  44. pointNumber: Union[int, None]
  45. pointNumbers: Union[List[int], None]
  46. pointIndex: Union[int, None]
  47. markerColor: Union[
  48. ItemOrList[
  49. ItemOrList[
  50. Union[
  51. float,
  52. int,
  53. str,
  54. None,
  55. ]
  56. ]
  57. ],
  58. None,
  59. ]
  60. markerSize: Union[
  61. ItemOrList[
  62. ItemOrList[
  63. Union[
  64. float,
  65. int,
  66. None,
  67. ]
  68. ]
  69. ],
  70. None,
  71. ]
  72. bbox: Union[BBox, None]
  73. class Plotly(NoSSRComponent):
  74. """Display a plotly graph."""
  75. library = "react-plotly.js@2.6.0"
  76. lib_dependencies: List[str] = ["plotly.js@2.35.3"]
  77. tag = "Plot"
  78. is_default = True
  79. # The figure to display. This can be a plotly figure or a plotly data json.
  80. data: Var[Figure] # pyright: ignore [reportInvalidTypeForm]
  81. # The layout of the graph.
  82. layout: Var[Dict]
  83. # The template for visual appearance of the graph.
  84. template: Var[Template] # pyright: ignore [reportInvalidTypeForm]
  85. # The config of the graph.
  86. config: Var[Dict]
  87. # If true, the graph will resize when the window is resized.
  88. use_resize_handler: Var[bool] = LiteralVar.create(True)
  89. # Fired after the plot is redrawn.
  90. on_after_plot: EventHandler[no_args_event_spec]
  91. # Fired after the plot was animated.
  92. on_animated: EventHandler[no_args_event_spec]
  93. # Fired while animating a single frame (does not currently pass data through).
  94. on_animating_frame: EventHandler[no_args_event_spec]
  95. # Fired when an animation is interrupted (to start a new animation for example).
  96. on_animation_interrupted: EventHandler[no_args_event_spec]
  97. # Fired when the plot is responsively sized.
  98. on_autosize: EventHandler[no_args_event_spec]
  99. # Fired whenever mouse moves over a plot.
  100. on_before_hover: EventHandler[no_args_event_spec]
  101. # Fired when a plotly UI button is clicked.
  102. on_button_clicked: EventHandler[no_args_event_spec]
  103. # Fired when the plot is clicked.
  104. on_click: EventHandler[_event_points_data_signature]
  105. # Fired when a selection is cleared (via double click).
  106. on_deselect: EventHandler[no_args_event_spec]
  107. # Fired when the plot is double clicked.
  108. on_double_click: EventHandler[no_args_event_spec]
  109. # Fired when a plot element is hovered over.
  110. on_hover: EventHandler[_event_points_data_signature]
  111. # Fired after the plot is laid out (zoom, pan, etc).
  112. on_relayout: EventHandler[no_args_event_spec]
  113. # Fired while the plot is being laid out.
  114. on_relayouting: EventHandler[no_args_event_spec]
  115. # Fired after the plot style is changed.
  116. on_restyle: EventHandler[no_args_event_spec]
  117. # Fired after the plot is redrawn.
  118. on_redraw: EventHandler[no_args_event_spec]
  119. # Fired after selecting plot elements.
  120. on_selected: EventHandler[_event_points_data_signature]
  121. # Fired while dragging a selection.
  122. on_selecting: EventHandler[_event_points_data_signature]
  123. # Fired while an animation is occurring.
  124. on_transitioning: EventHandler[no_args_event_spec]
  125. # Fired when a transition is stopped early.
  126. on_transition_interrupted: EventHandler[no_args_event_spec]
  127. # Fired when a hovered element is no longer hovered.
  128. on_unhover: EventHandler[_event_points_data_signature]
  129. def add_imports(self) -> dict[str, str]:
  130. """Add imports for the plotly component.
  131. Returns:
  132. The imports for the plotly component.
  133. """
  134. return {
  135. # For merging plotly data/layout/templates.
  136. "mergician@v2.0.2": "mergician"
  137. }
  138. def add_custom_code(self) -> list[str]:
  139. """Add custom codes for processing the plotly points data.
  140. Returns:
  141. Custom code snippets for the module level.
  142. """
  143. return [
  144. "const removeUndefined = (obj) => {Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key]); return obj}",
  145. """
  146. const extractPoints = (points) => {
  147. if (!points) return [];
  148. return points.map(point => {
  149. const bbox = point.bbox ? removeUndefined({
  150. x0: point.bbox.x0,
  151. x1: point.bbox.x1,
  152. y0: point.bbox.y0,
  153. y1: point.bbox.y1,
  154. z0: point.bbox.y0,
  155. z1: point.bbox.y1,
  156. }) : undefined;
  157. return removeUndefined({
  158. x: point.x,
  159. y: point.y,
  160. z: point.z,
  161. lat: point.lat,
  162. lon: point.lon,
  163. curveNumber: point.curveNumber,
  164. pointNumber: point.pointNumber,
  165. pointNumbers: point.pointNumbers,
  166. pointIndex: point.pointIndex,
  167. markerColor: point['marker.color'],
  168. markerSize: point['marker.size'],
  169. bbox: bbox,
  170. })
  171. })
  172. }
  173. """,
  174. ]
  175. @classmethod
  176. def create(cls, *children, **props) -> Component:
  177. """Create the Plotly component.
  178. Args:
  179. *children: The children of the component.
  180. **props: The properties of the component.
  181. Returns:
  182. The Plotly component.
  183. """
  184. from plotly.io import templates
  185. responsive_template = color_mode_cond(
  186. light=LiteralVar.create(templates["plotly"]),
  187. dark=LiteralVar.create(templates["plotly_dark"]),
  188. )
  189. if isinstance(responsive_template, Var):
  190. # Mark the conditional Var as a Template to avoid type mismatch
  191. responsive_template = responsive_template.to(Template)
  192. props.setdefault("template", responsive_template)
  193. return super().create(*children, **props)
  194. def _exclude_props(self) -> set[str]:
  195. # These props are handled specially in the _render function
  196. return {"data", "layout", "template"}
  197. def _render(self):
  198. tag = super()._render()
  199. figure = self.data.to(dict) if self.data is not None else Var.create({})
  200. merge_dicts = [] # Data will be merged and spread from these dict Vars
  201. if self.layout is not None:
  202. # Why is this not a literal dict? Great question... it didn't work
  203. # reliably because of how _var_name_unwrapped strips the outer curly
  204. # brackets if any of the contained Vars depend on state.
  205. layout_dict = LiteralVar.create({"layout": self.layout})
  206. merge_dicts.append(layout_dict)
  207. if self.template is not None:
  208. template_dict = LiteralVar.create({"layout": {"template": self.template}})
  209. merge_dicts.append(template_dict._without_data())
  210. if merge_dicts:
  211. tag.special_props.append(
  212. # Merge all dictionaries and spread the result over props.
  213. Var(
  214. _js_expr=f"{{...mergician({figure!s},"
  215. f"{','.join(str(md) for md in merge_dicts)})}}",
  216. ),
  217. )
  218. else:
  219. # Spread the figure dict over props, nothing to merge.
  220. tag.special_props.append(Var(_js_expr=f"{{...{figure!s}}}"))
  221. return tag
  222. CREATE_PLOTLY_COMPONENT: ImportDict = {
  223. "react-plotly.js": [
  224. ImportVar(
  225. tag="createPlotlyComponent",
  226. is_default=True,
  227. package_path="/factory",
  228. ),
  229. ]
  230. }
  231. def dynamic_plotly_import(name: str, package: str) -> str:
  232. """Create a dynamic import for a plotly component.
  233. Args:
  234. name: The name of the component.
  235. package: The package path of the component.
  236. Returns:
  237. The dynamic import for the plotly component.
  238. """
  239. return f"""
  240. const {name} = dynamic(() => import('{package}').then(mod => createPlotlyComponent(mod)), {{ssr: false}})
  241. """
  242. class PlotlyBasic(Plotly):
  243. """Display a basic plotly graph."""
  244. tag: str = "BasicPlotlyPlot"
  245. library = "react-plotly.js@2.6.0"
  246. lib_dependencies: list[str] = ["plotly.js-basic-dist-min@3.0.0"]
  247. def add_imports(self) -> ImportDict | list[ImportDict]:
  248. """Add imports for the plotly basic component.
  249. Returns:
  250. The imports for the plotly basic component.
  251. """
  252. return CREATE_PLOTLY_COMPONENT
  253. def _get_dynamic_imports(self) -> str:
  254. """Get the dynamic imports for the plotly basic component.
  255. Returns:
  256. The dynamic imports for the plotly basic component.
  257. """
  258. return dynamic_plotly_import(self.tag, "plotly.js-basic-dist-min")
  259. class PlotlyCartesian(Plotly):
  260. """Display a plotly cartesian graph."""
  261. tag: str = "CartesianPlotlyPlot"
  262. library = "react-plotly.js@2.6.0"
  263. lib_dependencies: list[str] = ["plotly.js-cartesian-dist-min@3.0.0"]
  264. def add_imports(self) -> ImportDict | list[ImportDict]:
  265. """Add imports for the plotly cartesian component.
  266. Returns:
  267. The imports for the plotly cartesian component.
  268. """
  269. return CREATE_PLOTLY_COMPONENT
  270. def _get_dynamic_imports(self) -> str:
  271. """Get the dynamic imports for the plotly cartesian component.
  272. Returns:
  273. The dynamic imports for the plotly cartesian component.
  274. """
  275. return dynamic_plotly_import(self.tag, "plotly.js-cartesian-dist-min")
  276. class PlotlyGeo(Plotly):
  277. """Display a plotly geo graph."""
  278. tag: str = "GeoPlotlyPlot"
  279. library = "react-plotly.js@2.6.0"
  280. lib_dependencies: list[str] = ["plotly.js-geo-dist-min@3.0.0"]
  281. def add_imports(self) -> ImportDict | list[ImportDict]:
  282. """Add imports for the plotly geo component.
  283. Returns:
  284. The imports for the plotly geo component.
  285. """
  286. return CREATE_PLOTLY_COMPONENT
  287. def _get_dynamic_imports(self) -> str:
  288. """Get the dynamic imports for the plotly geo component.
  289. Returns:
  290. The dynamic imports for the plotly geo component.
  291. """
  292. return dynamic_plotly_import(self.tag, "plotly.js-geo-dist-min")
  293. class PlotlyGl3d(Plotly):
  294. """Display a plotly 3d graph."""
  295. tag: str = "Gl3dPlotlyPlot"
  296. library = "react-plotly.js@2.6.0"
  297. lib_dependencies: list[str] = ["plotly.js-gl3d-dist-min@3.0.0"]
  298. def add_imports(self) -> ImportDict | list[ImportDict]:
  299. """Add imports for the plotly 3d component.
  300. Returns:
  301. The imports for the plotly 3d component.
  302. """
  303. return CREATE_PLOTLY_COMPONENT
  304. def _get_dynamic_imports(self) -> str:
  305. """Get the dynamic imports for the plotly 3d component.
  306. Returns:
  307. The dynamic imports for the plotly 3d component.
  308. """
  309. return dynamic_plotly_import(self.tag, "plotly.js-gl3d-dist-min")
  310. class PlotlyGl2d(Plotly):
  311. """Display a plotly 2d graph."""
  312. tag: str = "Gl2dPlotlyPlot"
  313. library = "react-plotly.js@2.6.0"
  314. lib_dependencies: list[str] = ["plotly.js-gl2d-dist-min@3.0.0"]
  315. def add_imports(self) -> ImportDict | list[ImportDict]:
  316. """Add imports for the plotly 2d component.
  317. Returns:
  318. The imports for the plotly 2d component.
  319. """
  320. return CREATE_PLOTLY_COMPONENT
  321. def _get_dynamic_imports(self) -> str:
  322. """Get the dynamic imports for the plotly 2d component.
  323. Returns:
  324. The dynamic imports for the plotly 2d component.
  325. """
  326. return dynamic_plotly_import(self.tag, "plotly.js-gl2d-dist-min")
  327. class PlotlyMapbox(Plotly):
  328. """Display a plotly mapbox graph."""
  329. tag: str = "MapboxPlotlyPlot"
  330. library = "react-plotly.js@2.6.0"
  331. lib_dependencies: list[str] = ["plotly.js-mapbox-dist-min@3.0.0"]
  332. def add_imports(self) -> ImportDict | list[ImportDict]:
  333. """Add imports for the plotly mapbox component.
  334. Returns:
  335. The imports for the plotly mapbox component.
  336. """
  337. return CREATE_PLOTLY_COMPONENT
  338. def _get_dynamic_imports(self) -> str:
  339. """Get the dynamic imports for the plotly mapbox component.
  340. Returns:
  341. The dynamic imports for the plotly mapbox component.
  342. """
  343. return dynamic_plotly_import(self.tag, "plotly.js-mapbox-dist-min")
  344. class PlotlyFinance(Plotly):
  345. """Display a plotly finance graph."""
  346. tag: str = "FinancePlotlyPlot"
  347. library = "react-plotly.js@2.6.0"
  348. lib_dependencies: list[str] = ["plotly.js-finance-dist-min@3.0.0"]
  349. def add_imports(self) -> ImportDict | list[ImportDict]:
  350. """Add imports for the plotly finance component.
  351. Returns:
  352. The imports for the plotly finance component.
  353. """
  354. return CREATE_PLOTLY_COMPONENT
  355. def _get_dynamic_imports(self) -> str:
  356. """Get the dynamic imports for the plotly finance component.
  357. Returns:
  358. The dynamic imports for the plotly finance component.
  359. """
  360. return dynamic_plotly_import(self.tag, "plotly.js-finance-dist-min")
  361. class PlotlyStrict(Plotly):
  362. """Display a plotly strict graph."""
  363. tag: str = "StrictPlotlyPlot"
  364. library = "react-plotly.js@2.6.0"
  365. lib_dependencies: list[str] = ["plotly.js-strict-dist-min@3.0.0"]
  366. def add_imports(self) -> ImportDict | list[ImportDict]:
  367. """Add imports for the plotly strict component.
  368. Returns:
  369. The imports for the plotly strict component.
  370. """
  371. return CREATE_PLOTLY_COMPONENT
  372. def _get_dynamic_imports(self) -> str:
  373. """Get the dynamic imports for the plotly strict component.
  374. Returns:
  375. The dynamic imports for the plotly strict component.
  376. """
  377. return dynamic_plotly_import(self.tag, "plotly.js-strict-dist-min")