{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "The Plotly pane displays [Plotly plots](https://plotly.com/python/) within a Panel application. It enhances the speed of plot updates by using binary serialization for array data contained in the Plotly object. \n", "\n", "It uses [plotly.js](https://plotly.com/javascript/) **version {{PLOTLY_VERSION}}** \n", "\n", "Please remember that to use the Plotly pane in a Jupyter notebook, you must activate the [Panel extension](https://panel.holoviz.org/api/cheatsheet.html#pn-extension) and include `\"plotly\"` as an argument. This step ensures that plotly.js is properly set up." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "pn.extension(\"plotly\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameters:\n", "\n", "For details on other options for customizing the component see the general [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides as well as the specific [Style Plotly Plots](../../how_to/styling/plotly.md) how-to guide.\n", "\n", "### Core\n", "\n", "* **``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", "### 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", "___" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import plotly.graph_objs as go\n", "\n", "import panel as pn\n", "\n", "pn.extension(\"plotly\")\n", "\n", "xx = np.linspace(-3.5, 3.5, 100)\n", "yy = np.linspace(-3.5, 3.5, 100)\n", "x, y = np.meshgrid(xx, yy)\n", "z = np.exp(-((x - 1) ** 2) - y**2) - (x**3 + y**4 - x / 5) * np.exp(-(x**2 + y**2))\n", "\n", "surface=go.Surface(z=z)\n", "fig = go.Figure(data=[surface])\n", "\n", "fig.update_layout(\n", " title=\"Plotly 3D Plot\",\n", " width=500,\n", " height=500,\n", " margin=dict(t=50, b=50, r=50, l=50),\n", ")\n", "\n", "plotly_pane = pn.pane.Plotly(fig)\n", "plotly_pane" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once created `Plotly` pane can be updated by assigning a new figure object" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "new_fig = go.Figure(data=[go.Surface(z=np.sin(z+1))])\n", "new_fig.update_layout(\n", " title=\"Plotly 3D Plot\",\n", " width=500,\n", " height=500,\n", " margin=dict(t=50, b=50, r=50, l=50),\n", ")\n", "\n", "plotly_pane.object = new_fig" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lets reset the Plotly pane" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plotly_pane.object = fig" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Layout Example\n", "\n", "The `Plotly` pane supports layouts and subplots of arbitrary complexity, allowing even deeply nested Plotly figures to be displayed:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "from plotly import subplots\n", "\n", "pn.extension(\"plotly\")\n", "\n", "heatmap = go.Heatmap(\n", " z=[[1, 20, 30],\n", " [20, 1, 60],\n", " [30, 60, 1]],\n", " showscale=False)\n", "\n", "y0 = np.random.randn(50)\n", "y1 = np.random.randn(50)+1\n", "\n", "box_1 = go.Box(y=y0)\n", "box_2 = go.Box(y=y1)\n", "data = [heatmap, box_1, box_2]\n", "\n", "fig_layout = subplots.make_subplots(\n", " rows=2, cols=2, specs=[[{}, {}], [{'colspan': 2}, None]],\n", " subplot_titles=('First Subplot','Second Subplot', 'Third Subplot')\n", ")\n", "\n", "fig_layout.append_trace(box_1, 1, 1)\n", "fig_layout.append_trace(box_2, 1, 2)\n", "fig_layout.append_trace(heatmap, 2, 1)\n", "\n", "fig_layout['layout'].update(height=600, width=600, title='i <3 subplots')\n", "\n", "pn.pane.Plotly(fig_layout)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Responsive Plots\n", "\n", "Plotly plots can be made responsive using the `autosize` option on a Plotly layout and a responsive `sizing_mode` argument to the `Plotly` pane:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import panel as pn\n", "import plotly.express as px\n", "\n", "pn.extension(\"plotly\")\n", "\n", "data = pd.DataFrame([\n", " ('Monday', 7), ('Tuesday', 4), ('Wednesday', 9), ('Thursday', 4),\n", " ('Friday', 4), ('Saturday', 4), ('Sunday', 4)], columns=['Day', 'Orders']\n", ")\n", "\n", "fig_responsive = px.line(data, x=\"Day\", y=\"Orders\")\n", "fig_responsive.update_traces(mode=\"lines+markers\", marker=dict(size=10), line=dict(width=4))\n", "fig_responsive.layout.autosize = True\n", "\n", "responsive = pn.pane.Plotly(fig_responsive, height=300)\n", "\n", "pn.Column('## A responsive plot', responsive, sizing_mode='stretch_width')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Plot Configuration\n", "\n", "You can set the [Plotly configuration options](https://plotly.com/javascript/configuration-options/) via the `config` parameter. Lets try to configure `scrollZoom`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "responsive_with_zoom = pn.pane.Plotly(fig_responsive, config={\"scrollZoom\": True}, height=300)\n", "\n", "pn.Column('## A responsive and scroll zoomable plot', responsive_with_zoom, sizing_mode='stretch_width')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Try scrolling with the mouse over the plot!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Patching\n", "\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 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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import plotly.graph_objs as go\n", "\n", "import panel as pn\n", "\n", "pn.extension(\"plotly\")\n", "\n", "xx = np.linspace(-3.5, 3.5, 100)\n", "yy = np.linspace(-3.5, 3.5, 100)\n", "x, y = np.meshgrid(xx, yy)\n", "z = np.exp(-((x - 1) ** 2) - y**2) - (x**3 + y**4 - x / 5) * np.exp(-(x**2 + y**2))\n", "\n", "surface = go.Surface(z=z)\n", "layout = go.Layout(\n", " title='Plotly 3D Plot',\n", " autosize=False,\n", " width=500,\n", " height=500,\n", " margin=dict(t=50, b=50, r=50, l=50)\n", ")\n", "\n", "fig_patch = dict(data=[surface], layout=layout)\n", "\n", "plotly_pane_patch = pn.pane.Plotly(fig_patch)\n", "plotly_pane_patch" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "surface.z = np.sin(z+1)\n", "plotly_pane_patch.object = fig_patch" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly, modifying the plot ``layout`` will only modify the layout, leaving the traces unaffected." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig_patch['layout']['width'] = 800\n", "\n", "plotly_pane_patch.object = fig_patch" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lets reset the Plotly pane" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "surface.z = z\n", "fig_patch['layout']['width'] = 500\n", "\n", "plotly_pane_patch.object = fig_patch" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 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_ohlc = 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", "stream_data = go.Ohlc(\n", " x=df_ohlc.loc[:start_index, \"Date\"],\n", " open=df_ohlc.loc[:start_index, \"AAPL.Open\"],\n", " high=df_ohlc.loc[:start_index, \"AAPL.High\"],\n", " low=df_ohlc.loc[:start_index, \"AAPL.Low\"],\n", " close=df_ohlc.loc[:start_index, \"AAPL.Close\"],\n", ")\n", "\n", "fig_stream = {\"data\": stream_data, \"layout\": go.Layout(xaxis_rangeslider_visible=False)}\n", "\n", "plotly_pane_stream = pn.pane.Plotly(fig_stream)\n", "\n", "\n", "def stream():\n", " index = len(stream_data.x)\n", " if index == len(df_ohlc):\n", " index = 0\n", "\n", " stream_data[\"x\"] = df_ohlc.loc[:index, \"Date\"]\n", " stream_data[\"open\"] = df_ohlc.loc[:index, \"AAPL.Open\"]\n", " stream_data[\"high\"] = df_ohlc.loc[:index, \"AAPL.High\"]\n", " stream_data[\"low\"] = df_ohlc.loc[:index, \"AAPL.Low\"]\n", " stream_data[\"close\"] = df_ohlc.loc[:index, \"AAPL.Close\"]\n", " plotly_pane_stream.object = fig_stream\n", "\n", "\n", "pn.state.add_periodic_callback(stream, period=100, count=50)\n", "\n", "plotly_pane_stream" ] }, { "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_ohlc = 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", "gen_data = go.Ohlc(\n", " x=df_ohlc.loc[:start_index, \"Date\"],\n", " open=df_ohlc.loc[:start_index, \"AAPL.Open\"],\n", " high=df_ohlc.loc[:start_index, \"AAPL.High\"],\n", " low=df_ohlc.loc[:start_index, \"AAPL.Low\"],\n", " close=df_ohlc.loc[:start_index, \"AAPL.Close\"],\n", ")\n", "layout = go.Layout(xaxis_rangeslider_visible=False)\n", "\n", "\n", "async def stream_generator():\n", " for _ in range(start_index, start_index+50):\n", " index = len(gen_data.x)\n", " if index == len(df_ohlc):\n", " index = 0\n", "\n", " gen_data[\"x\"] = df_ohlc.loc[:index, \"Date\"]\n", " gen_data[\"open\"] = df_ohlc.loc[:index, \"AAPL.Open\"]\n", " gen_data[\"high\"] = df_ohlc.loc[:index, \"AAPL.High\"]\n", " gen_data[\"low\"] = df_ohlc.loc[:index, \"AAPL.Low\"]\n", " gen_data[\"close\"] = df_ohlc.loc[:index, \"AAPL.Close\"]\n", " \n", " yield {\"data\": gen_data, \"layout\": layout}\n", " await sleep(0.05)\n", "\n", "\n", "pn.pane.Plotly(stream_generator)" ] }, { "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_in_place = 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_in_place.add_scatter(y=[2, 1, 4, 3])\n", " fig_in_place.add_bar(y=[2, 1, 4, 3])\n", " fig_in_place.layout.title = \"New Figure\"\n", " elif mod == 2:\n", " button.name = \"Reset\"\n", " scatter = fig_in_place.data[0]\n", " scatter.y = [3, 1, 4, 3]\n", " bar = fig_in_place.data[1]\n", " bar.y = [5, 3, 2, 8]\n", " fig_in_place.layout.title.text = \"Updated Figure\"\n", " else:\n", " fig_in_place.data = []\n", " fig_in_place.layout = {\"title\": \"\"}\n", " button.name = \"Create\"\n", "\n", "pn.bind(handle_click, button.param.clicks, watch=True)\n", "button.clicks=1\n", "\n", "plotly_pane_in_place = pn.pane.Plotly(\n", " fig_in_place,\n", " height=400,\n", " width=700,\n", " # link_figure=False\n", ")\n", "\n", "pn.Column(\n", " button,\n", " plotly_pane_in_place,\n", ")" ] }, { "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": [ "## 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_events = px.scatter(df, x='x', y='y', title='Click on a Point!', hover_name='x',)\n", "fig_events.update_traces(marker=dict(size=20))\n", "fig_events.update_layout(autosize=True, hovermode='closest')\n", "\n", "plotly_pane_event=pn.pane.Plotly(fig_events, 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_event.param.click_data)\n", "\n", "# Put things together\n", "pn.Column(plotly_pane_event, 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", "pn.Param(plotly_pane_event, parameters=event_parameters, max_width=1100, name=\"Plotly Event Parameters\")" ] }, { "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": {}, "source": [ "\n", "\n", "### Parent-Child Views\n", "\n", "A common technique for exploring higher-dimensional datasets is the use of Parent-Child views. This approach allows you to employ a top-down method to initially exing thehree most important dimensions in the parent plot. You can then select a subset of the data points and examine them in greater detail and across additional dimensions.\n", "\n", "Let's create a plot where Box or Lasso selections in the parent plot update a child plot. We will also customize the action bars using the `config` parameter." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import plotly.express as px\n", "\n", "import panel as pn\n", "\n", "pn.extension(\"plotly\")\n", "df = (\n", " pd.read_csv(\"https://datasets.holoviz.org/penguins/v1/penguins.csv\")\n", " .dropna()\n", " .reset_index(drop=True)\n", ")\n", "df[\"index\"] = df.index # Used to filter rows for child view\n", "\n", "color_map = {\"Adelie\": \"blue\", \"Chinstrap\": \"red\", \"Gentoo\": \"green\"}\n", "\n", "fig_parent = px.scatter(\n", " df,\n", " x=\"flipper_length_mm\",\n", " y=\"body_mass_g\",\n", " color_discrete_map=color_map,\n", " custom_data=\"index\", # Used to filter rows for child view\n", " color=\"species\",\n", " title=\"Parent Plot: Box or Lasso Select Points\",\n", ")\n", "\n", "\n", "def fig_child(selectedData):\n", " if selectedData:\n", " indices = [point[\"customdata\"][0] for point in selectedData[\"points\"]]\n", " filtered_df = df.iloc[indices]\n", " title = f\"Child Plot: Selected Points({len(indices)})\"\n", " else:\n", " filtered_df = df\n", " title = f\"Child Plot: All Points ({len(filtered_df)})\"\n", "\n", " fig = px.scatter(\n", " filtered_df,\n", " x=\"bill_length_mm\",\n", " y=\"bill_depth_mm\",\n", " color_discrete_map=color_map,\n", " color=\"species\",\n", " hover_data={\"flipper_length_mm\": True, \"body_mass_g\": True},\n", " title=title,\n", " )\n", " return fig\n", "\n", "\n", "parent_config = {\n", " \"modeBarButtonsToAdd\": [\"select2d\", \"lasso2d\"],\n", " \"modeBarButtonsToRemove\": [\n", " \"zoomIn2d\",\n", " \"zoomOut2d\",\n", " \"pan2d\",\n", " \"zoom2d\",\n", " \"autoScale2d\",\n", " ],\n", " \"displayModeBar\": True,\n", " \"displaylogo\": False,\n", "}\n", "parent_pane = pn.pane.Plotly(fig_parent, config=parent_config)\n", "\n", "ifig_child = pn.bind(fig_child, parent_pane.param.selected_data)\n", "child_config = {\n", " \"modeBarButtonsToRemove\": [\n", " \"select2d\",\n", " \"lasso2d\",\n", " \"zoomIn2d\",\n", " \"zoomOut2d\",\n", " \"pan2d\",\n", " \"zoom2d\",\n", " \"autoScale2d\",\n", " ],\n", " \"displayModeBar\": True,\n", " \"displaylogo\": False,\n", "}\n", "child_pane = pn.pane.Plotly(ifig_child, config=child_config)\n", "\n", "pn.FlexBox(parent_pane, child_pane)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Controls\n", "\n", "The `Plotly` pane exposes a number of options which can be changed from both Python and Javascript try out the effect of these parameters interactively:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Row(responsive.controls(jslink=True), responsive)" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }