From 12ddbe41b51610f7bcb17fcaee716b0c9d6a6ef9 Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Fri, 5 Jul 2024 13:21:27 -0500 Subject: [PATCH 01/10] Add solara dependency --- environment.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 6f8debd3c6a7e11b9ea6ee87176ae255b3295f22 Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Sun, 7 Jul 2024 12:46:38 -0500 Subject: [PATCH 02/10] Flesh out the solara notebook --- 04c_solara.ipynb | 364 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 357 insertions(+), 7 deletions(-) diff --git a/04c_solara.ipynb b/04c_solara.ipynb index cb3eac7..da31846 100644 --- a/04c_solara.ipynb +++ b/04c_solara.ipynb @@ -6,21 +6,371 @@ "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", + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "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": [ + "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": [ + "year_range = solara.reactive(year_range_input)\n", + "window_size = solara.reactive(2)\n", + "polynomial_order = solara.reactive(1)\n", "\n", + "# Print one of the values -- just like for an ipywidgete 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": [ + "# 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", - "## 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)" + "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": [ + "# 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": [ + "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": [ + "@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": [ + "## 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": [ + "@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": [ + "@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 according. 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": [ + "# 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": [ + "## Exercises\n", + "\n", + "1. 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", + "2. Improve the controls consistency check. 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 } From a7b8f00da6563b1e5bb0f162adbb73084f30b4b0 Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Sun, 7 Jul 2024 14:47:26 -0500 Subject: [PATCH 03/10] Add standard notebook intro and nbdev exports --- 04c_solara.ipynb | 65 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/04c_solara.ipynb b/04c_solara.ipynb index da31846..7029ba0 100644 --- a/04c_solara.ipynb +++ b/04c_solara.ipynb @@ -6,7 +6,26 @@ "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", + "\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 main " ] }, { @@ -15,6 +34,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "from pathlib import Path\n", "\n", "import pandas as pd\n", @@ -38,6 +58,8 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", + "\n", "DATA_DIR = 'data'\n", "DATA_FILE = 'land-ocean-temp-index.csv'\n", "\n", @@ -72,10 +94,18 @@ "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", - "\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 ipywidgete you use .value\n", "print(year_range.value)" ] @@ -97,6 +127,7 @@ "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", @@ -151,6 +182,8 @@ "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", @@ -179,6 +212,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "selected_df = solara.reactive(original_df.copy())" ] }, @@ -195,6 +229,8 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", + "\n", "@solara.component \n", "def selected_data():\n", " \"\"\"\n", @@ -226,6 +262,8 @@ "metadata": {}, "outputs": [], "source": [ + "#| export \n", + "\n", "@solara.component\n", "def draw_plot():\n", " plt.xlabel('Year')\n", @@ -256,6 +294,7 @@ "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", @@ -275,7 +314,7 @@ " # 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 according. Its child is an ipydatagrid.DataGrid, like we used previously.\n", + " # an accordian. 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", @@ -303,6 +342,24 @@ "Page()" ] }, + { + "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": {}, From ef031351ad8b2a6d57235f3e016981326deb950d Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Mon, 8 Jul 2024 14:34:39 -0700 Subject: [PATCH 04/10] Add initial dashboard display with solara --- 04c_solara.ipynb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/04c_solara.ipynb b/04c_solara.ipynb index 7029ba0..18c24ff 100644 --- a/04c_solara.ipynb +++ b/04c_solara.ipynb @@ -25,7 +25,7 @@ "metadata": {}, "outputs": [], "source": [ - "#| default_exp main " + "#| default_exp app " ] }, { @@ -106,7 +106,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Print one of the values -- just like for an ipywidgete you use .value\n", + "# Print one of the values -- just like for an ipywidgets you use .value\n", "print(year_range.value)" ] }, @@ -314,7 +314,7 @@ " # 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 accordian. Its child is an ipydatagrid.DataGrid, like we used previously.\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", @@ -360,6 +360,19 @@ "nb_export('04c_solara.ipynb', 'dashboard_solara')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Displaying the dashboard with solara\n", + "\n", + "Solara includes both the Python framework for writing widgets that we have talked about and a server for displaying notebooks. Copy/paste this into a terminal to run this dashboaard using solara:\n", + "\n", + "```bash\n", + "solara run 04c_solara.ipynb\n", + "```" + ] + }, { "cell_type": "markdown", "metadata": {}, From e8e9ca8c9a1f93284876d18eba12a43c65665125 Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Mon, 8 Jul 2024 14:34:51 -0700 Subject: [PATCH 05/10] Break up solara exercises --- 04c_solara.ipynb | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/04c_solara.ipynb b/04c_solara.ipynb index 18c24ff..fb58211 100644 --- a/04c_solara.ipynb +++ b/04c_solara.ipynb @@ -408,10 +408,32 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Exercises\n", + "## Solara exercises" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. 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": [ + "### 2. Improve the controls consistency check\n", "\n", - "1. 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", - "2. Improve the controls consistency check. 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." + "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." ] }, { From 9ae9ea10cf715369b62a6ab92e04c041546735db Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Mon, 8 Jul 2024 14:58:15 -0700 Subject: [PATCH 06/10] Better organize headings and exercises --- 04c_solara.ipynb | 50 ++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/04c_solara.ipynb b/04c_solara.ipynb index fb58211..cb9b801 100644 --- a/04c_solara.ipynb +++ b/04c_solara.ipynb @@ -10,6 +10,8 @@ "\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", + "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", @@ -249,7 +251,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Make the plot\n", + "## 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", @@ -281,7 +290,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Defining the dashboard\n", + "### 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", @@ -342,6 +351,22 @@ "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": {}, @@ -364,20 +389,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Displaying the dashboard with solara\n", - "\n", - "Solara includes both the Python framework for writing widgets that we have talked about and a server for displaying notebooks. Copy/paste this into a terminal to run this dashboaard using solara:\n", - "\n", - "```bash\n", - "solara run 04c_solara.ipynb\n", - "```" + "## Solara exercises" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Be careful, solara can be an effective foot gun\n", + "### 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" ] @@ -408,14 +427,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Solara exercises" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1. Add the text widgets\n", + "### 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" ] @@ -431,7 +443,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 2. Improve the controls consistency check\n", + "### 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." ] From 0b91c26c3c92edfcbe4582a9159962d5097a275b Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Mon, 8 Jul 2024 15:05:28 -0700 Subject: [PATCH 07/10] Add solara dashboard to key --- key/dashboard_solara/app.py | 141 ++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 key/dashboard_solara/app.py 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() + From c03a055a8623e4283c545183787d01e5e24a109f Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Mon, 8 Jul 2024 15:36:02 -0700 Subject: [PATCH 08/10] Add link to more extensive solara example --- 04c_solara.ipynb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/04c_solara.ipynb b/04c_solara.ipynb index cb9b801..2d82fa0 100644 --- a/04c_solara.ipynb +++ b/04c_solara.ipynb @@ -385,6 +385,17 @@ "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": {}, From 494f3859cbc2cab7d0c51c593ade446080af1b9c Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Mon, 8 Jul 2024 16:56:37 -0700 Subject: [PATCH 09/10] Add solara deployment notebook --- 04d_deployment_with_solara.ipynb | 181 +++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 04d_deployment_with_solara.ipynb diff --git a/04d_deployment_with_solara.ipynb b/04d_deployment_with_solara.ipynb new file mode 100644 index 0000000..82607b9 --- /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, apparently." + ] + }, + { + "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.y`, 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 +} From 2969cc1101ec85fecdda0b427ad81a7d5c3b5bc5 Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Mon, 8 Jul 2024 20:04:22 -0700 Subject: [PATCH 10/10] Apply suggestions from code review Co-authored-by: Juan Cabanela --- 04d_deployment_with_solara.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/04d_deployment_with_solara.ipynb b/04d_deployment_with_solara.ipynb index 82607b9..0d5238a 100644 --- a/04d_deployment_with_solara.ipynb +++ b/04d_deployment_with_solara.ipynb @@ -138,7 +138,7 @@ " + `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, apparently." + "+ The `ipyautoui` version of the dashboard is not on PyCafe because `ipyautoui` isn't supported (yet?)." ] }, { @@ -152,7 +152,7 @@ "\n", "Two things were uploaded to make the app:\n", "\n", - "+ `dashboard_solara/app.y`, modified so that `DATA = \".\"` instead of `DATA = \"data\"`\n", + "+ `dashboard_solara/app.py`, modified so that `DATA = \".\"` instead of `DATA = \"data\"`\n", "+ The CSV data file from the tutorial." ] }