plotly.py 15 KB

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