plotly.py 15 KB

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