Skip to content

Commit

Permalink
Describe Plotly Streaming and Events (#6760)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcSkovMadsen authored Apr 19, 2024
1 parent 7164013 commit d8d6061
Show file tree
Hide file tree
Showing 2 changed files with 321 additions and 21 deletions.
327 changes: 313 additions & 14 deletions examples/reference/panes/Plotly.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"___"
]
},
Expand All @@ -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,
Expand Down Expand Up @@ -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."
]
},
{
Expand Down Expand Up @@ -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:"
]
Expand Down Expand Up @@ -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": {},
Expand Down
Loading

0 comments on commit d8d6061

Please sign in to comment.