diff --git a/04c_solara.ipynb b/04c_solara.ipynb index cb3eac7..2d82fa0 100644 --- a/04c_solara.ipynb +++ b/04c_solara.ipynb @@ -6,21 +6,486 @@ "source": [ "# Introducing Solara\n", "\n", - "After doing all this work, we should also review another approach to the entire problem of developing web apps with Python. This approach is called Solara, and it is a Python library that allows you to create web applications using Python code only. This is a very interesting approach, as it allows you to create web applications without having to write any HTML, CSS, or JavaScript code. In this notebook, we will introduce Solara and show you how to create a simple web application using it.\n", + "### Goal of this notebook \n", "\n", + "After doing all this work, we should also review another approach to the entire problem of developing web apps with Python. This approach is called Solara, and it is both a Python library that allows you to create web applications using Python code only and a server to deploy those web apps.\n", "\n", - "## TO DO IN THIS NOTEBOOK\n", - "- [ ] Make sure what was GitHub Copilot generated above is actually accurate\n", - "- [ ] Introduce Solara and its approach to developing web apps\n", - "- [ ] Show how to recreate our dashboard using Solara (if easy enough)" + "Solara builds on ipywidgets but removes most of the burden of explicitly setting up and removing observers. The widgets it renders are generated by [`ipyvuetify`](https://github.com/widgetti/ipyvuetify). \n", + "\n", + "\n", + "### Steps you will take in this notebook\n", + "\n", + "1. Learn about *reactive* variables and solara *components*.\n", + "2. Build a version of our dashboard using solara.\n", + "3. Briefly discuss how a solara server differs from a voila server.\n", + "4. Discuss the steps to deploy a solara app on the [ploomber](https://www.platform.ploomber.io/) service.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp app " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "from pathlib import Path\n", + "\n", + "import pandas as pd\n", + "import solara\n", + "from ipydatagrid import DataGrid\n", + "from matplotlib.figure import Figure\n", + "from matplotlib import pyplot as plt\n", + "from scipy.signal import savgol_filter\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Read the data " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "DATA_DIR = 'data'\n", + "DATA_FILE = 'land-ocean-temp-index.csv'\n", + "\n", + "original_df = pd.read_csv(Path(DATA_DIR) / DATA_FILE, escapechar='#')\n", + "year_range_input = (min(original_df[\"Year\"]), max(original_df[\"Year\"]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set up the *reactive* variables and controls\n", + "\n", + "These allow solara to handle updating controls as values change." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Reactive variables for the controls\n", + "\n", + "The cell below defines three *reactive* variables. These are variables whose changes will be monitored for\n", + "changes by solara. Variables defined with `solara.reactive` are typically global variables used to manage the state of the application. It is possible to define reactive variables that are local to a function by using `solara.use_state` or `solar.use_reactive`.\n", + "\n", + "The argument to `solara.reactive` is the initial value of the variable. That initial value can have any type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "year_range = solara.reactive(year_range_input)\n", + "window_size = solara.reactive(2)\n", + "polynomial_order = solara.reactive(1)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Print one of the values -- just like for an ipywidgets you use .value\n", + "print(year_range.value)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Solara components for controls\n", + "\n", + "The cell below defines, in one function, a column of controls: 3 sliders, one for `year_range`, one for `window_size` and one for `polynomial_order`.\n", + "\n", + "*Components* are the building blocks out of which a solara application is built. You can make a function you write a component in solara by using the decorator `@solara.component`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "# Here we define our own component, called controls. A component can take arguments, \n", + "# though this one does not.\n", + "@solara.component\n", + "def controls():\n", + " \"\"\"\n", + " This panel contains the year_range, window_size and polynomial_order controls.\n", + " \"\"\"\n", + " # solar.Column() is another component defined in solara itself. Everything in the \n", + " # with block is arranged in a column.\n", + " with solara.Column() as crtl:\n", + " # SliderRangeInt is another solara component\n", + " solara.SliderRangeInt(\n", + " \"Range of years\", \n", + " # The line below is key -- it connects the slider to the reactive variable year_range\n", + " value=year_range, \n", + " min=year_range_input[0],\n", + " max=year_range_input[1],\n", + " )\n", + " \n", + " solara.SliderInt(\n", + " \"Window size\",\n", + " # Link this slider to window_size\n", + " value=window_size,\n", + " min=2,\n", + " max=100\n", + " )\n", + " solara.SliderInt(\n", + " \"Polynomial order\",\n", + " # Link this slider to polynomial_order\n", + " value=polynomial_order,\n", + " min=1,\n", + " max=10\n", + " )\n", + " # If there is a single displayable component in the function then solara will display that,\n", + " # otherwise it renders the return value.\n", + " return crtl\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Another component for limiting polynomial order\n", + "\n", + "The component below does not display anything on the screen. Instead, it checks for consistency between the `window_size` and `polynomial_order`. Because we have defined both of those as *reactive* variables, solara will automatically call this component, as long as we include a call to it in one of the components is displayed. We'll put that call in our \"main\" dashboard below, but it could be worked into the definition of our controls instead." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "# Registering as a component ensures this is called when either reactive variable's \n", + "# value changes.\n", + "@solara.component\n", + "def check_poly_order():\n", + " if polynomial_order.value > 10 or polynomial_order.value >= window_size.value:\n", + " polynomial_order.value = min(window_size.value - 1, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Reactive variables and components for the data" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The argument of `solara.reactive` can be anything, including a Pandas data frame. Declaring this as reactive ensures that solara responds when the selected data changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "selected_df = solara.reactive(original_df.copy())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The component below also does not display any data. The only thing it does is calculate a smoothing column and update `selected_df`. This will automatically be called when `year_range`, `window_size` or `polynomial_order` changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "@solara.component \n", + "def selected_data():\n", + " \"\"\"\n", + " This component only updates the selected data. Since selected_df is a reactive \n", + " variable, any component which 1) uses selected_df and 2) is rendered in a UI component\n", + " will automatically be updated.\n", + " \"\"\"\n", + " original_df['Smoothed Data'] = savgol_filter(original_df['Temperature'],\n", + " window_size.value,\n", + " polynomial_order.value).round(decimals=3)\n", + " selected_df.value = original_df[(original_df['Year'] >= year_range.value[0])\n", + " & (original_df['Year'] <= year_range.value[1])]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define the remaining widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Make the plot\n", + "\n", + "Either the pyplot or object interface to matplotlib can be used. If pyplot is used, then the plot should be closed after drawing it so that you do not end up with a bunch of open (but inaccessible) plots.\n", + "\n", + "Since we declared `selected_df` as a reactive variable, the plot is redrawn whenever its value changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export \n", + "\n", + "@solara.component\n", + "def draw_plot():\n", + " plt.xlabel('Year')\n", + " plt.ylabel('Temperature Anomalies over Land w.r.t. 1951-80 (˚C)')\n", + " plt.title('Global Annual Mean Surface Air Temperature Change')\n", + "\n", + " plt.plot(selected_df.value['Year'], selected_df.value['Temperature'], label='Raw Data')\n", + " plt.plot(selected_df.value['Year'], selected_df.value['Smoothed Data'], label='Smoothed Data')\n", + " plt.legend()\n", + " plt.show()\n", + " plt.close()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Defining the dashboard\n", + "\n", + "In the cell below we define the dashboard. We could call it anything we want, but when running it as a dashboard using `solara-server` if there is an object called `Page`, then that is what will be rendered in the browser.\n", + "\n", + "The overall layout will have one row with two columns. The first column has the controls and the second column has the data and graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@solara.component\n", + "def Page():\n", + " # These first two components are called here so that solara knows it should call them \n", + " # when changes occur in any of the reactive variables used in those components.\n", + " check_poly_order()\n", + " selected_data()\n", + " \n", + " # We make a row, which will end up with two columns \n", + " with solara.Row():\n", + " # Here we define the left column and restrict its width to 500px.\n", + " with solara.Column(style=dict(width=\"500px\")):\n", + " # Get some extra space at the top...\n", + " solara.Text(\"\\n\\n\")\n", + " # Here we use the controls component we defined above.\n", + " controls()\n", + " # Make column 2 with the data and graph. This column will use whatever space\n", + " # is available that the first column doesn't use.\n", + " with solara.Column():\n", + " # Display the data. The Details component is a collapsible component sort of like\n", + " # an accordion. Its child is an ipydatagrid.DataGrid, like we used previously.\n", + " # There is another option built in to solara called solara.DataFrame with similar \n", + " # functionality.\n", + " solara.Details(\n", + " summary=\"Click to show data\",\n", + " children=[DataGrid(selected_df.value)]\n", + " )\n", + " # This draws the plot. Solara undestands that this needs to be redrawn whenever selected_df\n", + " # changes.\n", + " draw_plot()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Display the dashboard" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Page()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Displaying the dashboard with solara server\n", + "\n", + "Solara includes both the Python framework for writing widgets that we have talked about and a server for displaying notebooks. It differs from [`voila`]() in some important ways: it only renders the `Page` object rather than rendering every cell and uses virtual kernels so that the notebooks load very quickly, and data is shared between instances.\n", + "\n", + "\n", + "Copy/paste this into a terminal to run this dashboard using solara:\n", + "\n", + "```bash\n", + "solara run 04c_solara.ipynb\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export with nbdev" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from nbdev.export import nb_export\n", + "\n", + "nb_export('04c_solara.ipynb', 'dashboard_solara')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## solara scales well to larger applications\n", + "\n", + "Open up this dashboard, which is more complicated than the one we have built: https://py.cafe/maartenbreddels/solara-dashboard-scatter\n", + "\n", + "You will be able to view both the code and the dashboard. The code for this example is just over 100 lines, not much longer than the solara version of our dashboard." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solara exercises" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Be careful, solara can be an effective foot gun\n", + "\n", + "See if you can spot the error in the code below -- why doesn't the range slider behave as expected?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "year_range_ex2 = solara.reactive((1880, 2023))\n", + "\n", + "@solara.component\n", + "def BadSlider():\n", + " solara.SliderRangeInt(\n", + " \"Some integer range\", \n", + " value=year_range_ex2, \n", + " min=year_range_ex2.value[0], \n", + " max=year_range_ex2.value[1],\n", + " tick_labels=True,\n", + " step=5\n", + " )\n", + "\n", + "BadSlider()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Add the text widgets\n", + "\n", + "Add the text widgets from the dashboard to this solara example. Use the [solara `HTML` component](https://solara.dev/documentation/components/output/html) to display the text.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Improve the controls consistency check\n", + "\n", + "The window size should really be at least one less than the range of years displayed. In other words, it does not make sense to use a window size of 100 if you are only displaying 50 years of data. It does not crash here when you do that because the smoothed column is called from the full data set." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/04d_deployment_with_solara.ipynb b/04d_deployment_with_solara.ipynb new file mode 100644 index 0000000..0d5238a --- /dev/null +++ b/04d_deployment_with_solara.ipynb @@ -0,0 +1,181 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d88809c4-b011-4bc0-8a71-e90e097716e3", + "metadata": {}, + "source": [ + "# Deploying widget apps with solara\n", + "\n", + "### Goal of this notebook \n", + "\n", + "There are a couple of interesting deployment options -- Ploomber and PyCafe -- available with solara that we will discuss. Although having to use solara might seem restrictive, we can turn our earlier dashboards into solara apps with just a couple of lines of code.\n", + "\n", + "\n", + "### Steps you will take in this notebook\n", + "\n", + "1. Learn how to turn our previous dashboards into solara apps.\n", + "2. Learn how to deploy a solara app with ploomber.\n", + "3. Deploy a solara app to PyCafe." + ] + }, + { + "cell_type": "markdown", + "id": "2ac530b9-dcf1-4e5d-9229-0f5ca7871e5a", + "metadata": {}, + "source": [ + "## Making any widget a solara app\n", + "\n", + "Since solara is built on ipywidgets, any existing widget can be turned into a solara app. As a first example, let's turn a simple ipywidget -- a dropdown -- into a solara app." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9eef8a1-0ec3-4afe-80df-3c9e74eff27f", + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "import solara" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4034af40-9a52-469a-9848-beeda4eba404", + "metadata": {}, + "outputs": [], + "source": [ + "drop = widgets.Dropdown(options=[\"a\", \"b\", \"c\"])" + ] + }, + { + "cell_type": "markdown", + "id": "56638de1-c5d5-4bdd-91e6-f177f4e7a632", + "metadata": {}, + "source": [ + "To make this dropdown into a solara app we just need to display the dropdown inside a solara component. By default, solara server will render the object called `Page` so that is what we name our component:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5e502de-57da-4e16-9e98-c491353cca90", + "metadata": {}, + "outputs": [], + "source": [ + "@solara.component\n", + "def Page():\n", + " solara.display(drop)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9fbdaf1e-e9ed-4ecc-b58a-1926b839f57b", + "metadata": {}, + "outputs": [], + "source": [ + "Page()" + ] + }, + { + "cell_type": "markdown", + "id": "c111793c-9f98-4a7a-9d13-22cad993719e", + "metadata": {}, + "source": [ + "Note that solara displays the dropdown as an ipywidgets dropdown rather than as an ipyvuetify widget, since we explicitly asked for an ipywidget dropdown." + ] + }, + { + "cell_type": "markdown", + "id": "73ee9beb-55ee-4a8a-aa4b-877b2cc99122", + "metadata": {}, + "source": [ + "**EXERCISE:**\n", + "\n", + "Import the first version of the dashboard we made and make it into a solara app. Call the object you make `Page` like we did above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "322b8f73-232d-4602-aea1-f4ccb3327ef3", + "metadata": {}, + "outputs": [], + "source": [ + "from dashboard.main import main_widget\n", + "\n", + "@solara.component\n", + "def Page():\n", + " # FILL THIS IN" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8b61836-32af-4940-b2b8-3bdb44f2dc9b", + "metadata": {}, + "outputs": [], + "source": [ + "Page()" + ] + }, + { + "cell_type": "markdown", + "id": "050d5ee9-dcfd-40be-ae20-70ae18912ce1", + "metadata": {}, + "source": [ + "## Deploying to PyCafe\n", + "\n", + "[PyCafe](https://py.cafe) is a web service that utilizes Pyodide to serve up a web app that runs entirely in your browser. There is no Python kernel running in the background, so it scales well. The service is still in beta so you should expect glitches. It is frequently necessary to use the \"System Restart\" menu option to get `scipy` to launch properly, for example. One other thing to be aware of: PyCafe is running in your broswer, so unless you frequently use the \"Push Changes to Cloud\" menu item any changes you have made are lost if your browser page reloads or if you do a \"System Restart\".\n", + "\n", + "Two versions of the dashboard in this tutorial have been deployed to PyCafe so that you can take a look at them.\n", + "\n", + "+ The pure-solara version of the dashboard is [here](https://py.cafe/mwcraig/solara-global-temp-analysis). The only modifcation necessary was to change the `DATA` folder variable from `data` to `.`, since PyCafe does not let you make folders.\n", + "+ The initial version of the dashboard is [here](https://py.cafe/mwcraig/plain-widget-2024-dashboard). A couple of modifications were necessary:\n", + " + `widgets.py` was renamed to `tutorial_widgets.py`\n", + " + `main.py` imports from `tutorial_widgets` instead of `dashboard.widgets` because i) folders are not allowed, and ii) `widgets` would conflict with `import ipywidgets as widgets`.\n", + " + The `DATA` folder was changed from `data` to `.`.\n", + "+ The `ipyautoui` version of the dashboard is not on PyCafe because `ipyautoui` isn't supported (yet?)." + ] + }, + { + "cell_type": "markdown", + "id": "69451d9e-43c8-4e14-976c-12faff3b237c", + "metadata": {}, + "source": [ + "## Deploying to Ploomber\n", + "\n", + "[Ploomber](https://ploomber.io/) is a service for deploying apps that has support for both voila and solara apps. It is backed by a server, so it will not scale well to a large number of users. A solara app should scale somewhat better than a voila app. In both cases, uploading an app is fairly easy. The solara version of the app is here: https://white-flower-7961.ploomberapp.io\n", + "\n", + "Two things were uploaded to make the app:\n", + "\n", + "+ `dashboard_solara/app.py`, modified so that `DATA = \".\"` instead of `DATA = \"data\"`\n", + "+ The CSV data file from the tutorial." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/environment.yml b/environment.yml index 52899af..8b65938 100644 --- a/environment.yml +++ b/environment.yml @@ -22,9 +22,10 @@ dependencies: - pip - pydantic - python=3.11 + - solara - statsmodels - traittypes - voici - voila>=0.5.6 - pip: - - ipyautoui \ No newline at end of file + - ipyautoui diff --git a/key/dashboard_solara/app.py b/key/dashboard_solara/app.py new file mode 100644 index 0000000..edc8671 --- /dev/null +++ b/key/dashboard_solara/app.py @@ -0,0 +1,141 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: ../04c_solara.ipynb. + +# %% auto 0 +__all__ = ['DATA_DIR', 'DATA_FILE', 'original_df', 'year_range_input', 'year_range', 'window_size', 'polynomial_order', + 'selected_df', 'controls', 'check_poly_order', 'selected_data', 'draw_plot', 'Page'] + +# %% ../04c_solara.ipynb 2 +from pathlib import Path + +import pandas as pd +import solara +from ipydatagrid import DataGrid +from matplotlib.figure import Figure +from matplotlib import pyplot as plt +from scipy.signal import savgol_filter + + +# %% ../04c_solara.ipynb 4 +DATA_DIR = 'data' +DATA_FILE = 'land-ocean-temp-index.csv' + +original_df = pd.read_csv(Path(DATA_DIR) / DATA_FILE, escapechar='#') +year_range_input = (min(original_df["Year"]), max(original_df["Year"])) + +# %% ../04c_solara.ipynb 7 +year_range = solara.reactive(year_range_input) +window_size = solara.reactive(2) +polynomial_order = solara.reactive(1) + + +# %% ../04c_solara.ipynb 10 +# Here we define our own component, called controls. A component can take arguments, +# though this one does not. +@solara.component +def controls(): + """ + This panel contains the year_range, window_size and polynomial_order controls. + """ + # solar.Column() is another component defined in solara itself. Everything in the + # with block is arranged in a column. + with solara.Column() as crtl: + # SliderRangeInt is another solara component + solara.SliderRangeInt( + "Range of years", + # The line below is key -- it connects the slider to the reactive variable year_range + value=year_range, + min=year_range_input[0], + max=year_range_input[1], + ) + + solara.SliderInt( + "Window size", + # Link this slider to window_size + value=window_size, + min=2, + max=100 + ) + solara.SliderInt( + "Polynomial order", + # Link this slider to polynomial_order + value=polynomial_order, + min=1, + max=10 + ) + # If there is a single displayable component in the function then solara will display that, + # otherwise it renders the return value. + return crtl + + + +# %% ../04c_solara.ipynb 12 +# Registering as a component ensures this is called when either reactive variable's +# value changes. +@solara.component +def check_poly_order(): + if polynomial_order.value > 10 or polynomial_order.value >= window_size.value: + polynomial_order.value = min(window_size.value - 1, 10) + +# %% ../04c_solara.ipynb 15 +selected_df = solara.reactive(original_df.copy()) + +# %% ../04c_solara.ipynb 17 +@solara.component +def selected_data(): + """ + This component only updates the selected data. Since selected_df is a reactive + variable, any component which 1) uses selected_df and 2) is rendered in a UI component + will automatically be updated. + """ + original_df['Smoothed Data'] = savgol_filter(original_df['Temperature'], + window_size.value, + polynomial_order.value).round(decimals=3) + selected_df.value = original_df[(original_df['Year'] >= year_range.value[0]) + & (original_df['Year'] <= year_range.value[1])] + + +# %% ../04c_solara.ipynb 20 +@solara.component +def draw_plot(): + plt.xlabel('Year') + plt.ylabel('Temperature Anomalies over Land w.r.t. 1951-80 (˚C)') + plt.title('Global Annual Mean Surface Air Temperature Change') + + plt.plot(selected_df.value['Year'], selected_df.value['Temperature'], label='Raw Data') + plt.plot(selected_df.value['Year'], selected_df.value['Smoothed Data'], label='Smoothed Data') + plt.legend() + plt.show() + plt.close() + + +# %% ../04c_solara.ipynb 22 +@solara.component +def Page(): + # These first two components are called here so that solara knows it should call them + # when changes occur in any of the reactive variables used in those components. + check_poly_order() + selected_data() + + # We make a row, which will end up with two columns + with solara.Row(): + # Here we define the left column and restrict its width to 500px. + with solara.Column(style=dict(width="500px")): + # Get some extra space at the top... + solara.Text("\n\n") + # Here we use the controls component we defined above. + controls() + # Make column 2 with the data and graph. This column will use whatever space + # is available that the first column doesn't use. + with solara.Column(): + # Display the data. The Details component is a collapsible component sort of like + # an accordion. Its child is an ipydatagrid.DataGrid, like we used previously. + # There is another option built in to solara called solara.DataFrame with similar + # functionality. + solara.Details( + summary="Click to show data", + children=[DataGrid(selected_df.value)] + ) + # This draws the plot. Solara undestands that this needs to be redrawn whenever selected_df + # changes. + draw_plot() +