526 lines
15 KiB
Python
526 lines
15 KiB
Python
"""Component for displaying a plotly graph."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any, TypedDict, TypeVar
|
|
|
|
from reflex_base.components.component import Component, NoSSRComponent, field
|
|
from reflex_base.event import EventHandler, no_args_event_spec
|
|
from reflex_base.utils import console
|
|
from reflex_base.utils.imports import ImportDict, ImportVar
|
|
from reflex_base.vars.base import LiteralVar, Var
|
|
from reflex_components_core.core.cond import color_mode_cond
|
|
|
|
try:
|
|
from plotly.graph_objs import Figure
|
|
from plotly.graph_objs.layout import Template
|
|
|
|
except ImportError:
|
|
console.warn("Plotly is not installed. Please run `pip install plotly`.")
|
|
if not TYPE_CHECKING:
|
|
Figure = Any
|
|
Template = Any
|
|
|
|
|
|
def _event_points_data_signature(e0: Var) -> tuple[Var[list[Point]]]:
|
|
"""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(_js_expr=f"extractPoints({e0}?.points)"),)
|
|
|
|
|
|
T = TypeVar("T")
|
|
|
|
ItemOrList = T | list[T]
|
|
|
|
|
|
class BBox(TypedDict):
|
|
"""Bounding box for a point in a plotly graph."""
|
|
|
|
x0: float | int | None
|
|
x1: float | int | None
|
|
y0: float | int | None
|
|
y1: float | int | None
|
|
z0: float | int | None
|
|
z1: float | int | None
|
|
|
|
|
|
class Point(TypedDict):
|
|
"""A point in a plotly graph."""
|
|
|
|
x: float | int | None
|
|
y: float | int | None
|
|
z: float | int | None
|
|
lat: float | int | None
|
|
lon: float | int | None
|
|
curveNumber: int | None
|
|
pointNumber: int | None
|
|
pointNumbers: list[int] | None
|
|
pointIndex: int | None
|
|
markerColor: ItemOrList[ItemOrList[float | int | str | None]] | None
|
|
markerSize: ItemOrList[ItemOrList[float | int | None,]] | None
|
|
bbox: BBox | None
|
|
|
|
|
|
class Plotly(NoSSRComponent):
|
|
"""Display a plotly graph."""
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js@3.5.0"]
|
|
|
|
tag = "Plot"
|
|
|
|
is_default = True
|
|
|
|
data: Var[Figure] = field(
|
|
doc="The figure to display. This can be a plotly figure or a plotly data json."
|
|
)
|
|
|
|
layout: Var[dict] = field(doc="The layout of the graph.")
|
|
|
|
template: Var[Template] = field(
|
|
doc="The template for visual appearance of the graph."
|
|
)
|
|
|
|
config: Var[dict] = field(doc="The config of the graph.")
|
|
|
|
use_resize_handler: Var[bool] = field(
|
|
default=LiteralVar.create(True),
|
|
doc="If true, the graph will resize when the window is resized.",
|
|
)
|
|
|
|
on_after_plot: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired after the plot is redrawn."
|
|
)
|
|
|
|
on_animated: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired after the plot was animated."
|
|
)
|
|
|
|
on_animating_frame: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired while animating a single frame (does not currently pass data through)."
|
|
)
|
|
|
|
on_animation_interrupted: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired when an animation is interrupted (to start a new animation for example)."
|
|
)
|
|
|
|
on_autosize: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired when the plot is responsively sized."
|
|
)
|
|
|
|
on_before_hover: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired whenever mouse moves over a plot."
|
|
)
|
|
|
|
on_button_clicked: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired when a plotly UI button is clicked."
|
|
)
|
|
|
|
on_click: EventHandler[_event_points_data_signature] = field(
|
|
doc="Fired when the plot is clicked."
|
|
)
|
|
|
|
on_deselect: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired when a selection is cleared (via double click)."
|
|
)
|
|
|
|
on_double_click: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired when the plot is double clicked."
|
|
)
|
|
|
|
on_hover: EventHandler[_event_points_data_signature] = field(
|
|
doc="Fired when a plot element is hovered over."
|
|
)
|
|
|
|
on_relayout: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired after the plot is laid out (zoom, pan, etc)."
|
|
)
|
|
|
|
on_relayouting: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired while the plot is being laid out."
|
|
)
|
|
|
|
on_restyle: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired after the plot style is changed."
|
|
)
|
|
|
|
on_redraw: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired after the plot is redrawn."
|
|
)
|
|
|
|
on_selected: EventHandler[_event_points_data_signature] = field(
|
|
doc="Fired after selecting plot elements."
|
|
)
|
|
|
|
on_selecting: EventHandler[_event_points_data_signature] = field(
|
|
doc="Fired while dragging a selection."
|
|
)
|
|
|
|
on_transitioning: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired while an animation is occurring."
|
|
)
|
|
|
|
on_transition_interrupted: EventHandler[no_args_event_spec] = field(
|
|
doc="Fired when a transition is stopped early."
|
|
)
|
|
|
|
on_unhover: EventHandler[_event_points_data_signature] = field(
|
|
doc="Fired when a hovered element is no longer hovered."
|
|
)
|
|
|
|
def add_imports(self) -> dict[str, str]:
|
|
"""Add imports for the plotly component.
|
|
|
|
Returns:
|
|
The imports for the plotly component.
|
|
"""
|
|
return {
|
|
# For merging plotly data/layout/templates.
|
|
"mergician@v2.0.2": "mergician"
|
|
}
|
|
|
|
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,
|
|
markerColor: point['marker.color'],
|
|
markerSize: point['marker.size'],
|
|
bbox: bbox,
|
|
})
|
|
})
|
|
}
|
|
""",
|
|
]
|
|
|
|
@classmethod
|
|
def create(cls, *children, **props) -> Component:
|
|
"""Create the Plotly component.
|
|
|
|
Args:
|
|
*children: The children of the component.
|
|
**props: The properties of the component.
|
|
|
|
Returns:
|
|
The Plotly component.
|
|
"""
|
|
from plotly.graph_objs.layout import Template
|
|
from plotly.io import templates
|
|
|
|
responsive_template = color_mode_cond(
|
|
light=LiteralVar.create(templates["plotly"]),
|
|
dark=LiteralVar.create(templates["plotly_dark"]),
|
|
)
|
|
if isinstance(responsive_template, Var):
|
|
# Mark the conditional Var as a Template to avoid type mismatch
|
|
responsive_template = responsive_template.to(Template)
|
|
props.setdefault("template", responsive_template)
|
|
return super().create(*children, **props)
|
|
|
|
def _exclude_props(self) -> set[str]:
|
|
# These props are handled specially in the _render function
|
|
return {"data", "layout", "template"}
|
|
|
|
def _render(self):
|
|
tag = super()._render()
|
|
figure = self.data.to(dict) if self.data is not None else Var.create({})
|
|
merge_dicts = [] # Data will be merged and spread from these dict Vars
|
|
if self.layout is not None:
|
|
# Why is this not a literal dict? Great question... it didn't work
|
|
# reliably because of how _var_name_unwrapped strips the outer curly
|
|
# brackets if any of the contained Vars depend on state.
|
|
layout_dict = LiteralVar.create({"layout": self.layout})
|
|
merge_dicts.append(layout_dict)
|
|
if self.template is not None:
|
|
template_dict = LiteralVar.create({"layout": {"template": self.template}})
|
|
merge_dicts.append(template_dict._without_data())
|
|
if merge_dicts:
|
|
tag = tag.set(
|
|
special_props=[
|
|
*tag.special_props,
|
|
# Merge all dictionaries and spread the result over props.
|
|
Var(
|
|
_js_expr=f"{{...mergician({figure!s},"
|
|
f"{','.join(str(md) for md in merge_dicts)})}}",
|
|
),
|
|
]
|
|
)
|
|
else:
|
|
tag = tag.set(
|
|
special_props=[
|
|
*tag.special_props,
|
|
# Spread the figure dict over props, nothing to merge.
|
|
Var(_js_expr=str(figure)),
|
|
]
|
|
)
|
|
return tag
|
|
|
|
|
|
CREATE_PLOTLY_COMPONENT: ImportDict = {
|
|
"react-plotly.js": [
|
|
ImportVar(
|
|
tag="createPlotlyComponent",
|
|
is_default=True,
|
|
package_path="/factory",
|
|
),
|
|
]
|
|
}
|
|
|
|
|
|
def dynamic_plotly_import(name: str, package: str) -> str:
|
|
"""Create a dynamic import for a plotly component.
|
|
|
|
Args:
|
|
name: The name of the component.
|
|
package: The package path of the component.
|
|
|
|
Returns:
|
|
The dynamic import for the plotly component.
|
|
"""
|
|
library_import = f"import('{package}')"
|
|
mod_import = ".then((mod) => createPlotlyComponent(mod))"
|
|
return f"""
|
|
const {name} = ClientSide(() =>
|
|
{library_import}{mod_import}
|
|
)
|
|
"""
|
|
|
|
|
|
class PlotlyBasic(Plotly):
|
|
"""Display a basic plotly graph."""
|
|
|
|
tag: str = "BasicPlotlyPlot"
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js-basic-dist-min@3.5.0"]
|
|
|
|
def add_imports(self) -> ImportDict | list[ImportDict]:
|
|
"""Add imports for the plotly basic component.
|
|
|
|
Returns:
|
|
The imports for the plotly basic component.
|
|
"""
|
|
return CREATE_PLOTLY_COMPONENT
|
|
|
|
def _get_dynamic_imports(self) -> str:
|
|
"""Get the dynamic imports for the plotly basic component.
|
|
|
|
Returns:
|
|
The dynamic imports for the plotly basic component.
|
|
"""
|
|
return dynamic_plotly_import(self.tag, "plotly.js-basic-dist-min")
|
|
|
|
|
|
class PlotlyCartesian(Plotly):
|
|
"""Display a plotly cartesian graph."""
|
|
|
|
tag: str = "CartesianPlotlyPlot"
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js-cartesian-dist-min@3.5.0"]
|
|
|
|
def add_imports(self) -> ImportDict | list[ImportDict]:
|
|
"""Add imports for the plotly cartesian component.
|
|
|
|
Returns:
|
|
The imports for the plotly cartesian component.
|
|
"""
|
|
return CREATE_PLOTLY_COMPONENT
|
|
|
|
def _get_dynamic_imports(self) -> str:
|
|
"""Get the dynamic imports for the plotly cartesian component.
|
|
|
|
Returns:
|
|
The dynamic imports for the plotly cartesian component.
|
|
"""
|
|
return dynamic_plotly_import(self.tag, "plotly.js-cartesian-dist-min")
|
|
|
|
|
|
class PlotlyGeo(Plotly):
|
|
"""Display a plotly geo graph."""
|
|
|
|
tag: str = "GeoPlotlyPlot"
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js-geo-dist-min@3.5.0"]
|
|
|
|
def add_imports(self) -> ImportDict | list[ImportDict]:
|
|
"""Add imports for the plotly geo component.
|
|
|
|
Returns:
|
|
The imports for the plotly geo component.
|
|
"""
|
|
return CREATE_PLOTLY_COMPONENT
|
|
|
|
def _get_dynamic_imports(self) -> str:
|
|
"""Get the dynamic imports for the plotly geo component.
|
|
|
|
Returns:
|
|
The dynamic imports for the plotly geo component.
|
|
"""
|
|
return dynamic_plotly_import(self.tag, "plotly.js-geo-dist-min")
|
|
|
|
|
|
class PlotlyGl3d(Plotly):
|
|
"""Display a plotly 3d graph."""
|
|
|
|
tag: str = "Gl3dPlotlyPlot"
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js-gl3d-dist-min@3.5.0"]
|
|
|
|
def add_imports(self) -> ImportDict | list[ImportDict]:
|
|
"""Add imports for the plotly 3d component.
|
|
|
|
Returns:
|
|
The imports for the plotly 3d component.
|
|
"""
|
|
return CREATE_PLOTLY_COMPONENT
|
|
|
|
def _get_dynamic_imports(self) -> str:
|
|
"""Get the dynamic imports for the plotly 3d component.
|
|
|
|
Returns:
|
|
The dynamic imports for the plotly 3d component.
|
|
"""
|
|
return dynamic_plotly_import(self.tag, "plotly.js-gl3d-dist-min")
|
|
|
|
|
|
class PlotlyGl2d(Plotly):
|
|
"""Display a plotly 2d graph."""
|
|
|
|
tag: str = "Gl2dPlotlyPlot"
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js-gl2d-dist-min@3.5.0"]
|
|
|
|
def add_imports(self) -> ImportDict | list[ImportDict]:
|
|
"""Add imports for the plotly 2d component.
|
|
|
|
Returns:
|
|
The imports for the plotly 2d component.
|
|
"""
|
|
return CREATE_PLOTLY_COMPONENT
|
|
|
|
def _get_dynamic_imports(self) -> str:
|
|
"""Get the dynamic imports for the plotly 2d component.
|
|
|
|
Returns:
|
|
The dynamic imports for the plotly 2d component.
|
|
"""
|
|
return dynamic_plotly_import(self.tag, "plotly.js-gl2d-dist-min")
|
|
|
|
|
|
class PlotlyMapbox(Plotly):
|
|
"""Display a plotly mapbox graph."""
|
|
|
|
tag: str = "MapboxPlotlyPlot"
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js-mapbox-dist-min@3.5.0"]
|
|
|
|
def add_imports(self) -> ImportDict | list[ImportDict]:
|
|
"""Add imports for the plotly mapbox component.
|
|
|
|
Returns:
|
|
The imports for the plotly mapbox component.
|
|
"""
|
|
return CREATE_PLOTLY_COMPONENT
|
|
|
|
def _get_dynamic_imports(self) -> str:
|
|
"""Get the dynamic imports for the plotly mapbox component.
|
|
|
|
Returns:
|
|
The dynamic imports for the plotly mapbox component.
|
|
"""
|
|
return dynamic_plotly_import(self.tag, "plotly.js-mapbox-dist-min")
|
|
|
|
|
|
class PlotlyFinance(Plotly):
|
|
"""Display a plotly finance graph."""
|
|
|
|
tag: str = "FinancePlotlyPlot"
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js-finance-dist-min@3.5.0"]
|
|
|
|
def add_imports(self) -> ImportDict | list[ImportDict]:
|
|
"""Add imports for the plotly finance component.
|
|
|
|
Returns:
|
|
The imports for the plotly finance component.
|
|
"""
|
|
return CREATE_PLOTLY_COMPONENT
|
|
|
|
def _get_dynamic_imports(self) -> str:
|
|
"""Get the dynamic imports for the plotly finance component.
|
|
|
|
Returns:
|
|
The dynamic imports for the plotly finance component.
|
|
"""
|
|
return dynamic_plotly_import(self.tag, "plotly.js-finance-dist-min")
|
|
|
|
|
|
class PlotlyStrict(Plotly):
|
|
"""Display a plotly strict graph."""
|
|
|
|
tag: str = "StrictPlotlyPlot"
|
|
|
|
library = "react-plotly.js@2.6.0"
|
|
|
|
lib_dependencies: list[str] = ["plotly.js-strict-dist-min@3.5.0"]
|
|
|
|
def add_imports(self) -> ImportDict | list[ImportDict]:
|
|
"""Add imports for the plotly strict component.
|
|
|
|
Returns:
|
|
The imports for the plotly strict component.
|
|
"""
|
|
return CREATE_PLOTLY_COMPONENT
|
|
|
|
def _get_dynamic_imports(self) -> str:
|
|
"""Get the dynamic imports for the plotly strict component.
|
|
|
|
Returns:
|
|
The dynamic imports for the plotly strict component.
|
|
"""
|
|
return dynamic_plotly_import(self.tag, "plotly.js-strict-dist-min")
|