diff --git a/examples/reference/panes/Plotly.ipynb b/examples/reference/panes/Plotly.ipynb index bbb47f2297..1e772deea8 100644 --- a/examples/reference/panes/Plotly.ipynb +++ b/examples/reference/panes/Plotly.ipynb @@ -33,22 +33,27 @@ "* **``object``** (object): The Plotly `Figure` or dictionary object being displayed.\n", "* **``config``** (dict): Additional configuration of the plot. See [Plotly configuration options](https://plotly.com/javascript/configuration-options/).\n", "\n", - "### Callbacks\n", - "\n", - "* **``click_data``** (dict): Click callback data\n", - "* **``clickannotation_data``** (dict): Clickannotation callback data\n", - "* **``hover_data``** (dict): Hover callback data\n", - "* **``link_figure``** (bool): Attach callbacks to the Plotly figure to update output when it is modified in place.\n", - "* **``relayout_data``** (dict): Relayout callback data\n", - "* **``restyle_data``** (dict): Restyle callback data\n", - "* **``selected_data``** (dict): Selected callback data\n", - "* **``viewport``** (dict): Current viewport state, i.e. the x- and y-axis limits of the displayed plot.\n", + "### Update in Place\n", + "\n", + "* **``link_figure``** (bool, default: True): Update the displayed Plotly figure when the Plotly `Figure` is modified in place.\n", + "\n", + "### Events\n", + "\n", + "* **``click_data``** (dict): Click event data from `plotly_click` event.\n", + "* **``clickannotation_data``** (dict): Clickannotation event data from `plotly_clickannotation` event.\n", + "* **``hover_data``** (dict): Hover event data from `plotly_hover` and `plotly_unhover` events.\n", + "* **``relayout_data``** (dict): Relayout event data from `plotly_relayout` event\n", + "* **``restyle_data``** (dict): Restyle event data from `plotly_restyle` event\n", + "* **``selected_data``** (dict): Selected event data from `plotly_selected` and `plotly_deselect` events.\n", + "* **``viewport``** (dict): Current viewport state, i.e. the x- and y-axis limits of the displayed plot. Updated on `plotly_relayout`, `plotly_relayouting` and `plotly_restyle` events.\n", "* **``viewport_update_policy``** (str, default = 'mouseup'): Policy by which the viewport parameter is updated during user interactions\n", " * ``mouseup``: updates are synchronized when mouse button is released after panning\n", " * ``continuous``: updates are synchronized continually while panning\n", " * ``throttle``: updates are synchronized while panning, at intervals determined by the viewport_update_throttle parameter\n", "* **``viewport_update_throttle``** (int, default = 200, bounds = (0, None)): Time interval in milliseconds at which viewport updates are synchronized when viewport_update_policy is \"throttle\".\n", "\n", + "### Other\n", + "\n", "___" ] }, @@ -59,6 +64,15 @@ "As with most other types ``Panel`` will automatically convert a Plotly `Figure` to a `Plotly` pane if it is passed to the `pn.panel` function or a Panel layout, but a `Plotly` pane can also be constructed directly using the `pn.pane.Plotly` constructor:" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic Example\n", + "\n", + "Lets create a basic example" + ] + }, { "cell_type": "code", "execution_count": null, @@ -135,11 +149,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Efficient Updates with Dictionaries\n", + "## Patching\n", "\n", - "Instead of updating the entire Figure you can efficiently update specific traces or the layout if you use a dictionary instead of a Plotly Figure.\n", + "Instead of updating the entire Figure you can efficiently patch traces or the layout if you use a dictionary instead of a Plotly Figure.\n", "\n", - "Note updates will only be fast if the ``Figure`` is defined as a dictionary, since Plotly will make copies of the traces, which means that modifying them in place has no effect. Modifying an array will send just the array using a binary protocol, leading to fast and efficient updates." + "Note patching will only be efficient if the ``Figure`` is defined as a dictionary, since Plotly will make copies of the traces, which means that modifying them in place has no effect. Modifying an array will send just the array using a binary protocol, leading to fast and efficient updates." ] }, { @@ -226,7 +240,198 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Plotly Layouts\n", + "## Streaming\n", + "\n", + "You can stream updates by replacing the entire Figure object. To stream efficiently though you should use patching technique described above.\n", + "\n", + "You can stream periodically using `pn.state.add_periodic_callback`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import plotly.graph_objects as go\n", + "\n", + "import panel as pn\n", + "\n", + "pn.extension(\"plotly\")\n", + "\n", + "\n", + "df = pn.cache(pd.read_csv)(\n", + " \"https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv\"\n", + ")\n", + "\n", + "start_index = 50\n", + "\n", + "data = go.Ohlc(\n", + " x=df.loc[:start_index, \"Date\"],\n", + " open=df.loc[:start_index, \"AAPL.Open\"],\n", + " high=df.loc[:start_index, \"AAPL.High\"],\n", + " low=df.loc[:start_index, \"AAPL.Low\"],\n", + " close=df.loc[:start_index, \"AAPL.Close\"],\n", + ")\n", + "\n", + "fig = {\"data\": data, \"layout\": go.Layout(xaxis_rangeslider_visible=False)}\n", + "\n", + "plotly_pane = pn.pane.Plotly(fig)\n", + "\n", + "\n", + "def stream():\n", + " index = len(data.x)\n", + " if index == len(df):\n", + " index = 0\n", + "\n", + " data[\"x\"] = df.loc[:index, \"Date\"]\n", + " data[\"open\"] = df.loc[:index, \"AAPL.Open\"]\n", + " data[\"high\"] = df.loc[:index, \"AAPL.High\"]\n", + " data[\"low\"] = df.loc[:index, \"AAPL.Low\"]\n", + " data[\"close\"] = df.loc[:index, \"AAPL.Close\"]\n", + " plotly_pane.object = fig\n", + "\n", + "\n", + "pn.state.add_periodic_callback(stream, period=100, count=50)\n", + "\n", + "plotly_pane" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also stream via a generator or async generator function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from asyncio import sleep\n", + "\n", + "import pandas as pd\n", + "import plotly.graph_objects as go\n", + "\n", + "import panel as pn\n", + "\n", + "pn.extension(\"plotly\")\n", + "\n", + "\n", + "df = pn.cache(pd.read_csv)(\n", + " \"https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv\"\n", + ")\n", + "\n", + "start_index = 50\n", + "\n", + "data = go.Ohlc(\n", + " x=df.loc[:start_index, \"Date\"],\n", + " open=df.loc[:start_index, \"AAPL.Open\"],\n", + " high=df.loc[:start_index, \"AAPL.High\"],\n", + " low=df.loc[:start_index, \"AAPL.Low\"],\n", + " close=df.loc[:start_index, \"AAPL.Close\"],\n", + ")\n", + "layout = go.Layout(xaxis_rangeslider_visible=False)\n", + "\n", + "\n", + "async def stream():\n", + " for _ in range(0, 50):\n", + " index = len(data.x)\n", + " if index == len(df):\n", + " index = 0\n", + "\n", + " data[\"x\"] = df.loc[:index, \"Date\"]\n", + " data[\"open\"] = df.loc[:index, \"AAPL.Open\"]\n", + " data[\"high\"] = df.loc[:index, \"AAPL.High\"]\n", + " data[\"low\"] = df.loc[:index, \"AAPL.Low\"]\n", + " data[\"close\"] = df.loc[:index, \"AAPL.Close\"]\n", + " \n", + " yield {\"data\": data, \"layout\": layout}\n", + " await sleep(0.05)\n", + "\n", + "\n", + "pn.pane.Plotly(stream)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Update in Place\n", + "\n", + "An alternative to updating the figure dictionary is updating the Plotly `Figure` in place, i.e. via its attributes and methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.graph_objects as go\n", + "\n", + "import panel as pn\n", + "\n", + "pn.extension(\"plotly\")\n", + "\n", + "\n", + "fig = go.Figure()\n", + "\n", + "button = pn.widgets.Button(name=\"Create\", button_type=\"primary\")\n", + "\n", + "\n", + "def handle_click(clicks):\n", + " mod = clicks % 3\n", + " if mod == 1:\n", + " button.name = \"Update\"\n", + " fig.add_scatter(y=[2, 1, 4, 3])\n", + " fig.add_bar(y=[2, 1, 4, 3])\n", + " fig.layout.title = \"New Figure\"\n", + " elif mod == 2:\n", + " button.name = \"Reset\"\n", + " scatter = fig.data[0]\n", + " scatter.y = [3, 1, 4, 3]\n", + " bar = fig.data[1]\n", + " bar.y = [5, 3, 2, 8]\n", + " fig.layout.title.text = \"Updated Figure\"\n", + " else:\n", + " fig.data = []\n", + " fig.layout = {\"title\": \"\"}\n", + " button.name = \"Create\"\n", + "\n", + "\n", + "pn.bind(handle_click, button.param.clicks, watch=True)\n", + "\n", + "plotly_pane = pn.pane.Plotly(\n", + " fig,\n", + " height=400,\n", + " width=700,\n", + " # link_figure=False\n", + ")\n", + "\n", + "pn.Column(\n", + " button,\n", + " plotly_pane,\n", + ").servable()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This enables you to use the Plotly `Figure` in the same way as you would have been using the Plotly [`FigureWidget`](https://plotly.com/python/figurewidget/).\n", + "\n", + "If you for some reason want to disable in place updates, you can set `link_figure=False` when you create the `Plotly` pane. You cannot change this when the pane has been created." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Layouts\n", "\n", "The `Plotly` pane supports layouts and subplots of arbitrary complexity, allowing even deeply nested Plotly figures to be displayed:" ] @@ -367,6 +572,100 @@ "pn.Column('## A responsive and zoomable plot', responsive, sizing_mode='stretch_width')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Try scrolling with the mouse!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Events\n", + "\n", + "The Plotly pane enables you to bind to most of the click, hover, selection and other events described in [Plotly Event Handlers](https://plotly.com/javascript/plotlyjs-events/).\n", + "\n", + "### Simple Event Example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.express as px\n", + "import panel as pn\n", + "import pandas as pd\n", + "\n", + "pn.extension(\"plotly\")\n", + "\n", + "# Create dataframe\n", + "x = [1, 2, 3, 4, 5]\n", + "y = [10, 20, 30, 20, 10]\n", + "df = pd.DataFrame({'x': x, 'y': y})\n", + "\n", + "# Create scatter plot\n", + "fig = px.scatter(df, x='x', y='y', title='Click on Points', hover_name='x',)\n", + "fig.update_traces(marker=dict(size=20))\n", + "fig.update_layout(autosize=True, hovermode='closest')\n", + "\n", + "plotly_pane=pn.pane.Plotly(fig, height=400, max_width=1200, sizing_mode=\"stretch_width\")\n", + "\n", + "# Define Child View\n", + "def child_view(event):\n", + " if not event:\n", + " return \"No point clicked\"\n", + " try:\n", + " point = event[\"points\"][0]\n", + " index = point['pointIndex']\n", + " x = point['x']\n", + " y = point['y']\n", + " except Exception as ex:\n", + " return f\"You clicked the Plotly Chart! I could not determine the point: {ex}\"\n", + " \n", + " return f\"**You clicked point {index} at ({x}, {y}) on the Plotly Chart!**\"\n", + "\n", + "ichild_view = pn.bind(child_view, plotly_pane.param.click_data)\n", + "\n", + "# Put things together\n", + "pn.Column(plotly_pane, ichild_view)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Event Inspection\n", + "\n", + "The be able to work with the events its a good idea to inspect them. You can do that by printing them or including them in your visualization.\n", + "\n", + "Lets display them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "event_parameters = [\"click_data\", \"click_annotation_data\", \"hover_data\", \"relayout_data\", \"restyle_data\", \"selected_data\", \"viewport\"]\n", + "\n", + "with pn.config.set(sizing_mode=\"stretch_width\"):\n", + " pane = pn.Param(plotly_pane, parameters=event_parameters, max_width=1100, name=\"Plotly Event Parameters\")\n", + "\n", + "pane" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the plot above, try hovering, clicking, selecting and changing the viewport by panning. Watch how the parameter values just above changes." + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/panel/pane/plotly.py b/panel/pane/plotly.py index 0934485069..a07d6a0e71 100644 --- a/panel/pane/plotly.py +++ b/panel/pane/plotly.py @@ -44,26 +44,27 @@ class Plotly(ModelPane): >>> Plotly(some_plotly_figure, width=500, height=500) """ - click_data = param.Dict(doc="Click callback data") + click_data = param.Dict(doc="Click event data from `plotly_click` event.") - clickannotation_data = param.Dict(doc="Clickannotation callback data") + clickannotation_data = param.Dict(doc="Clickannotation event data from `plotly_clickannotation` event.") config = param.Dict(nested_refs=True, doc=""" Plotly configuration options. See https://plotly.com/javascript/configuration-options/""") - hover_data = param.Dict(doc="Hover callback data") + hover_data = param.Dict(doc="Hover event data from `plotly_hover` and `plotly_unhover` events.") link_figure = param.Boolean(default=True, doc=""" Attach callbacks to the Plotly figure to update output when it is modified in place.""") - relayout_data = param.Dict(nested_refs=True, doc="Relayout callback data") + relayout_data = param.Dict(nested_refs=True, doc="Relayout event data from `plotly_relayout` event") - restyle_data = param.List(nested_refs=True, doc="Restyle callback data") + restyle_data = param.List(nested_refs=True, doc="Restyle event data from `plotly_restyle` event") - selected_data = param.Dict(nested_refs=True, doc="Selected callback data") + selected_data = param.Dict(nested_refs=True, doc="Selected event data from `plotly_selected` and `plotly_deselect` events.") - viewport = param.Dict(nested_refs=True, doc="Current viewport state, i.e. the x- and y-axis limits of the displayed plot.") + viewport = param.Dict(nested_refs=True, doc="""Current viewport state, i.e. the x- and y-axis limits of the displayed plot. + Updated on `plotly_relayout`, `plotly_relayouting` and `plotly_restyle` events.""") viewport_update_policy = param.Selector(default="mouseup", doc=""" Policy by which the viewport parameter is updated during user interactions.