{ "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", "Please remember that to use the Plotly pane in a Jupyter notebook, you must activate the Panel 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 = 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_stream = {\"data\": 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(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_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 = 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_generator():\n", " for _ in range(start_index, start_index+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_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 }