diff --git a/solara/components/misc.py b/solara/components/misc.py index c16fdcd27..5419783a7 100644 --- a/solara/components/misc.py +++ b/solara/components/misc.py @@ -272,30 +272,32 @@ def FigurePlotly( on_click: Callable[[Any], None] = None, on_hover: Callable[[Any], None] = None, on_unhover: Callable[[Any], None] = None, + on_relayout: Callable[[Any], None] = None, dependencies=None, ): from plotly.graph_objs._figurewidget import FigureWidget def on_points_callback(data): - if data: - event_type = data["event_type"] - if event_type == "plotly_click": - if on_click: - on_click(data) - elif event_type == "plotly_hover": - if on_hover: - on_hover(data) - elif event_type == "plotly_unhover": - if on_unhover: - on_unhover(data) - elif event_type == "plotly_selected": - if on_selection: - on_selection(data) - elif event_type == "plotly_deselect": - if on_deselect: - on_deselect(data) - - fig_element = FigureWidget.element(on__js2py_pointsCallback=on_points_callback) + if not data: + return + + event_type = data["event_type"] + event_mapping = { + "plotly_click": on_click, + "plotly_hover": on_hover, + "plotly_unhover": on_unhover, + "plotly_selected": on_selection, + "plotly_deselect": on_deselect + } + + callback = event_mapping.get(event_type) + if callback: + callback(data) + + fig_element = FigureWidget.element( + on__js2py_pointsCallback=on_points_callback, + on__js2py_relayout=on_relayout + ) def update_data(): fig_widget: FigureWidget = solara.get_widget(fig_element) diff --git a/solara/website/pages/__init__.py b/solara/website/pages/__init__.py index 0e9cccf17..dbcd3377b 100644 --- a/solara/website/pages/__init__.py +++ b/solara/website/pages/__init__.py @@ -6,7 +6,6 @@ from ..components import Header, Hero - title = "Home" route_order = ["/", "showcase", "docs", "api", "examples", "apps"] diff --git a/solara/website/pages/examples/visualization/annotator.py b/solara/website/pages/examples/visualization/annotator.py new file mode 100644 index 000000000..d2037f959 --- /dev/null +++ b/solara/website/pages/examples/visualization/annotator.py @@ -0,0 +1,68 @@ +"""# Image annotation with Solara + +This example displays how to annotate images with different drawing tools in plotly figures. Use the canvas +below to draw shapes and visualize the canvas callback. + + +Check [plotly docs](https://dash.plotly.com/annotations) for more information about image annotation. +""" +import json + +import plotly.graph_objects as go + +import solara + +title = "Plotly Image Annotator" +shapes = solara.reactive(None) + + +class CustomEncoder(json.JSONEncoder): + """ + Custom JSON encoder for Plotly objects. + + Plotly may return objects that the standard JSON encoder can't handle. This + encoder converts such objects to str, allowing serialization by json.dumps + """ + + def default(self, o): + if isinstance(o, object): + return str(o) + return super().default(o) + + +@solara.component +def Page(): + def on_relayout(data): + if data is None: + return + + relayout_data = data["relayout_data"] + + if "shapes" in relayout_data: + shapes.value = relayout_data["shapes"] + + fig = go.FigureWidget( + layout=go.Layout( + showlegend=False, + autosize=False, + width=600, + height=600, + dragmode="drawrect", + modebar={ + "add": [ + "drawclosedpath", + "drawcircle", + "drawrect", + "eraseshape", + ] + }, + ) + ) + + solara.FigurePlotly(fig, on_relayout=on_relayout) + if not shapes.value: + solara.Markdown("## Draw on the canvas") + else: + solara.Markdown("## Data returned by drawing") + formatted_shapes = str(json.dumps(shapes.value, indent=2, cls=CustomEncoder)) + solara.Preformatted(formatted_shapes) diff --git a/tests/integration/server_test.py b/tests/integration/server_test.py index 73cdf50a4..338a67b5e 100644 --- a/tests/integration/server_test.py +++ b/tests/integration/server_test.py @@ -33,10 +33,13 @@ def test_docs_basics(page_session: playwright.sync_api.Page, solara_server, sola page_session.locator("text=Exponent").wait_for() page_session.screenshot(path="tmp/screenshot_bqplot.png") - page_session.locator("text=Plotly").first.click() + page_session.locator("text=Scatter plot using Plotly").first.click() page_session.locator("text=plotly express").first.wait_for() page_session.screenshot(path="tmp/screenshot_plotly.png") + page_session.locator("text=Plotly Image Annotator").first.click() + page_session.locator("text=how to annotate images with").first.wait_for() + @solara.component def ClickButton():