From e265f8b7e95aebdee6380363a525881d90075ccb Mon Sep 17 00:00:00 2001 From: Daniel Chen Date: Tue, 12 Mar 2024 14:48:07 -0700 Subject: [PATCH 1/3] input task button component entry --- .../inputs/input-task-button/app-core.py | 37 +++++++ .../input-task-button/app-detail-preview.py | 37 +++++++ .../inputs/input-task-button/app-express.py | 35 ++++++ .../inputs/input-task-button/app-preview.py | 22 ++++ ...-variation-input-task-button-multi-core.py | 53 +++++++++ ...riation-input-task-button-multi-express.py | 53 +++++++++ components/inputs/input-task-button/index.qmd | 103 ++++++++++++++++++ 7 files changed, 340 insertions(+) create mode 100644 components/inputs/input-task-button/app-core.py create mode 100644 components/inputs/input-task-button/app-detail-preview.py create mode 100644 components/inputs/input-task-button/app-express.py create mode 100644 components/inputs/input-task-button/app-preview.py create mode 100644 components/inputs/input-task-button/app-variation-input-task-button-multi-core.py create mode 100644 components/inputs/input-task-button/app-variation-input-task-button-multi-express.py create mode 100644 components/inputs/input-task-button/index.qmd diff --git a/components/inputs/input-task-button/app-core.py b/components/inputs/input-task-button/app-core.py new file mode 100644 index 00000000..0fb23aee --- /dev/null +++ b/components/inputs/input-task-button/app-core.py @@ -0,0 +1,37 @@ +import asyncio # << +import datetime + +from shiny import App, Inputs, Outputs, Session, reactive, render, ui + +app_ui = ui.page_fluid( + ui.p("The time is ", ui.output_text("current_time", inline=True)), + ui.hr(), + ui.input_task_button("btn", "Square 5 slowly"), # << + ui.output_text("sq"), +) + + +def server(input: Inputs, output: Outputs, session: Session): + + @render.text + def current_time(): + reactive.invalidate_later(1) + return datetime.datetime.now().strftime("%H:%M:%S %p") + + @ui.bind_task_button(button_id="btn") # << + @reactive.extended_task # << + async def sq_value(x): # << + await asyncio.sleep(2) # << + return x**2 # << + + @reactive.effect # << + @reactive.event(input.btn) # << + def btn_click(): + sq_value(5) # << + + @render.text + def sq(): + return f"5 squared is: {str(sq_value.result())}" + + +app = App(app_ui, server) diff --git a/components/inputs/input-task-button/app-detail-preview.py b/components/inputs/input-task-button/app-detail-preview.py new file mode 100644 index 00000000..6ba98bf8 --- /dev/null +++ b/components/inputs/input-task-button/app-detail-preview.py @@ -0,0 +1,37 @@ +import asyncio +import datetime + +from shiny import App, Inputs, Outputs, Session, reactive, render, ui + +app_ui = ui.page_fluid( + ui.p("The time is ", ui.output_text("current_time", inline=True)), + ui.hr(), + ui.input_task_button("btn", "Square 5 slowly"), + ui.output_text("sq"), +) + + +def server(input: Inputs, output: Outputs, session: Session): + + @render.text + def current_time(): + reactive.invalidate_later(1) + return datetime.datetime.now().strftime("%H:%M:%S %p") + + @ui.bind_task_button(button_id="btn") + @reactive.extended_task + async def sq_value(x): + await asyncio.sleep(2) + return x**2 + + @reactive.effect + @reactive.event(input.btn) + def btn_click(): + sq_value(5) + + @render.text + def sq(): + return f"5 squared is: {str(sq_value.result())}" + + +app = App(app_ui, server) diff --git a/components/inputs/input-task-button/app-express.py b/components/inputs/input-task-button/app-express.py new file mode 100644 index 00000000..123ff445 --- /dev/null +++ b/components/inputs/input-task-button/app-express.py @@ -0,0 +1,35 @@ +import asyncio # << +import datetime + +from shiny import reactive +from shiny.express import render, input, ui + + +@render.text +def current_time(): + reactive.invalidate_later(1) + time_str = datetime.datetime.now().strftime("%H:%M:%S %p") + return f"The time is, {time_str}" + + +ui.hr() + +ui.input_task_button("btn", "Square 5 slowly") # << + + +@ui.bind_task_button(button_id="btn") # << +@reactive.extended_task # << +async def sq_values(x): # << + await asyncio.sleep(2) # << + return x**2 # << + + +@reactive.effect # << +@reactive.event(input.btn) # << +def btn_click(): # << + sq_values(5) # << + + +@render.text +def sq(): + return str(sq_values.result()) diff --git a/components/inputs/input-task-button/app-preview.py b/components/inputs/input-task-button/app-preview.py new file mode 100644 index 00000000..c5719d61 --- /dev/null +++ b/components/inputs/input-task-button/app-preview.py @@ -0,0 +1,22 @@ +import asyncio +from shiny import App, Inputs, Outputs, Session, reactive, ui + +app_ui = ui.page_fluid( + ui.input_task_button("btn", "Add numbers slowly"), +) + + +def server(input: Inputs, output: Outputs, session: Session): + + @ui.bind_task_button(button_id="btn") + @reactive.extended_task + async def long_calculation(): + await asyncio.sleep(1) + + @reactive.effect + @reactive.event(input.btn) + def btn_click(): + long_calculation() + + +app = App(app_ui, server) diff --git a/components/inputs/input-task-button/app-variation-input-task-button-multi-core.py b/components/inputs/input-task-button/app-variation-input-task-button-multi-core.py new file mode 100644 index 00000000..9f7d99da --- /dev/null +++ b/components/inputs/input-task-button/app-variation-input-task-button-multi-core.py @@ -0,0 +1,53 @@ +import asyncio +import datetime + +import matplotlib.pyplot as plt +import numpy as np + +from shiny import App, Inputs, Outputs, Session, reactive, render, ui + +app_ui = ui.page_fluid( + ui.input_numeric("x", "x", value=5), + ui.input_task_button("btn", "Square number slowly"), + ui.output_text("sq"), + ui.hr(), + ui.p( + "While computing, the time updates and you can still interact with the histogram." + ), + ui.p("The time is ", ui.output_text("current_time", inline=True)), + ui.input_slider("n", "Number of observations", min=0, max=1000, value=500), + ui.output_plot("plot"), +) + + +def server(input: Inputs, output: Outputs, session: Session): + @render.plot(alt="A histogram") + def plot(): + np.random.seed(19680801) + x = 100 + 15 * np.random.randn(input.n()) + fig, ax = plt.subplots() + ax.hist(x, bins=30, density=True) + return fig + + @render.text + def current_time(): + reactive.invalidate_later(1) + return datetime.datetime.now().strftime("%H:%M:%S %p") + + @ui.bind_task_button(button_id="btn") + @reactive.extended_task + async def sq_values(x): + await asyncio.sleep(5) + return x**2 + + @reactive.effect + @reactive.event(input.btn) + def btn_click(): + sq_values(input.x()) + + @render.text + def sq(): + return str(sq_values.result()) + + +app = App(app_ui, server) diff --git a/components/inputs/input-task-button/app-variation-input-task-button-multi-express.py b/components/inputs/input-task-button/app-variation-input-task-button-multi-express.py new file mode 100644 index 00000000..5fa1314f --- /dev/null +++ b/components/inputs/input-task-button/app-variation-input-task-button-multi-express.py @@ -0,0 +1,53 @@ +import asyncio +import datetime + +import matplotlib.pyplot as plt +import numpy as np + +from shiny import reactive +from shiny.express import render, input, ui + +ui.input_numeric("x", "x", value=5), +ui.input_task_button("btn", "Square number slowly") + + +@ui.bind_task_button(button_id="btn") +@reactive.extended_task +async def sq_values(x): + await asyncio.sleep(5) + return x**2 + + +@reactive.effect +@reactive.event(input.btn) +def btn_click(): + sq_values(input.x()) + + +@render.text +def sq(): + return str(sq_values.result()) + + +ui.hr() + +ui.p("While computing, the time updates and you can still interact with the histogram.") + + +@render.text +def current_time(): + reactive.invalidate_later(1) + dt_str = datetime.datetime.now().strftime("%H:%M:%S %p") + return f"The time is {dt_str}" + + +ui.input_slider("n", "Number of observations", min=0, max=1000, value=500), + + +@render.plot(alt="A histogram") +def plot(): + np.random.seed(19680801) + x = 100 + 15 * np.random.randn(input.n()) + fig, ax = plt.subplots() + ax.hist(x, bins=30, density=True) + return fig diff --git a/components/inputs/input-task-button/index.qmd b/components/inputs/input-task-button/index.qmd new file mode 100644 index 00000000..ff3375cd --- /dev/null +++ b/components/inputs/input-task-button/index.qmd @@ -0,0 +1,103 @@ +--- +title: Input Task Button +sidebar: components +appPreview: + file: components/inputs/input-task-button/app-preview.py +listing: +- id: example + template: ../../_partials/components-detail-example.ejs + template-params: + dir: components/inputs/input-task-button/ + contents: + - title: Preview + file: app-detail-preview.py + height: 200 + - title: Express + file: app-express.py + shinylive: https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuJZYDELADzCAOhDoNmLACZQKZOnAkSAZo2IwW7ABY0I2FlKatGcKISUA3FRA1ad+wxjgAPVOfbtj9Uy3MIWThGPGMIVABXMjDImlUICQABQODGDAo3MglgtRZCSMZAsgB9JXgACgBKRAl+fnNLGzgMA2soABsaeQoSjoUQioBGKrr68rgS9jJGFgBeOQGJjB64ZYhiAHdqjGnGNQmKsTAAUgAJRBOAWUuAZRYT1GPRiHqAtcLXtWOAFV04FgTYzsMIgCZTGYAX2OCQkcQwukY1QS8IMUVKZA4AGsSgAjaJkUhHMC4siJfAsY63ACOkSg5hYAFYdB0th1sM9+EJRLCIEl4biDLIyti8QSifiyISICVunNjqTyVUuSJxHzGlYaLZXFlKMFhZj2FiVTzoJxuHI4Hl2NSSu0OpE4OwKm4aia1W8oJsoDRWBwuDxiLsOnA4KgKgAmZWCVVjBofRivNwAKmTEfdvJSFk12qtajgVgz6uzzVctnIFTR0Qwiuj3LVuRYipKhC6hCx1UQRbeNrtnUdzsZddjiVHWaCIQy7myE+t1M7cfeZE+OhmFV79oHGC8kQ6ZGqLzAkIAukA + - title: Core + file: app-core.py + shinylive: https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuJZYDELADzCAOhDoNmLACZQKZOnAkSAZo2IwW7ABY0I2FlKasAgujwsAkhFQBXMuysB5Rw6dWAynHbteEFaMcFCESgBucEGUsnCMVvY0qtDoAPqJLAC8LIlYUADmcKlqADaJsgAUEvz8uahVYAAqunAsSvDG7Cxi+Dk0GMTujqkUAB5kDYT2jMHkI8o9VgYlBnCZjYz2cACU23jVNbm6jBV7B7X9Bh4jHADWqQBGjmSkDQ9kEIvdYF4AjvZQYIsACsOhKxAA7iVsD09vwhKJzn0BkMyCM4OMGuxfrD9hBtskJLE1Do4pETldHIgbHZHM4WIMyB5qW4mXSrOxfP5SNSfH4AttEMkaiwAAKzWKMDBjMhI4ksKYzSho9pwU5CiAikXBUIROAYAzhKAreQUVIlBRxCoARgJmq1-GCZGmmtNcFVGDdHogkNOGHYZEYalVDQApAAJRChgCyUa8LFDqFhwpqotyDwMshu7HuTzILwgFTzBdSNFkmR670+YG28JE4nt-HFITCNEiGAxFAgsSzZDudcRjbYnG4cjgJOxqSNZTVo0FA4bDv4UAhUBorA4XB4xH9JTgcHqACZa4J60jte6XSxRgAqG+Hhcpps61vt8dqOBhR9D5u6tv6uBInICpKTIDAqxPBFFxqeUq1SQgVkIW51XPGpJ2nLYKmBSCz0+H8JTiaVOzlccdF+FChwvZ1GE1NQelBbEAWCWROmpEAAxOdDjS2DBgnYewSgmXYAF8ekJFJUCyFgLHqNBUHSGgOTJOI7TAYSAF0gA +- id: relevant-functions + template: ../../_partials/components-detail-relevant-functions.ejs + contents: + - title: ui.input_task_button + href: https://shiny.posit.co/py/api/ui.input_task_button.html + signature: ui.input_task_button(id, label, *args, icon=None, label_busy='Processing...', icon_busy=MISSING, width=None, type='primary', auto_reset=True, **kwargs) + - title: reactive.extended_task + href: https://shiny.posit.co/py/api/reactive.extended_task.html + signature: reactive.extended_task(func=None) + - title: reactive.event + href: https://shiny.posit.co/py/api/reactive.event.html + signature: reactive.event(*args, ignore_none=True, ignore_init=False) + - title: reactive.effect + href: https://shiny.posit.co/py/api/reactive.effect.html + signature: reactive.effect(fn=None, *, suspended=False, priority=0, session=MISSING) +- id: variations + template: ../../_partials/components-variations.ejs + template-params: + dir: components/inputs/input-task-button/ + contents: + - title: Input task button with inputs and interactivity + description: Here's an application that shows how the input task button can perform non-blocking computations. + It will take in 2 inputs to add together in a slow computation and you will will be able to move the slider + to make histogram modifications and see the time increment during the computation time. + apps: + - title: Preview + file: app-variation-input-task-button-multi-core.py + - title: Express + file: app-variation-input-task-button-multi-express.py + shinylive: https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuIB0IdBsxYATKBTJ04AgUKasYE1ABtiZFTQBGWbKvVt2LVWTn0FLCAFd62Q5dSyIAM0bEYLdgAsaEO-JFGOChCKQA3GRc3D29fbAw4AA9UIPYjANYgiFE4RjwWX1QrMnyrGicyjELigH1reEYaQgAKPjBEtvy2jvwWMKgVKzgAXgBWAEo8AUrqshqyDgBrGq1islJWsC0yCE6WNoBlAEcrKCDLGy1czzUAdxVsNvGnAQABSq1fUXmllbWN1ZkdYQGo0UTDNrbXZgZ4QV5BELhOAJRIUbJwb4LdiLAQcLiEMRwZyeI41fqDODsZqJcaIAQsBlsW5QGisPHcXgYdgqOBwVDNCb0xlBMhWRgQFiJABUUoATC84QjQjQIglnM44KE3kqkQkIuRmrMMFDYTliVCaoRNIRFs1aUKGexSeShlSjYk7bCnPDKDlGBgKKiBGaSXa6RLhXBReLPGRGM0nWSBq6MKkrCoyJ6FZUvPGvdYaFhNgB1Hw8lgkejFXwAc3yZC8cBYUngLCsqHEFCMUGyLGwxCsFZ7sZoKhUBXIuURLFurK8zcbLB87HWNcYsAwTwVPvR-sDpnRxMIYqycxbcDDDpYOpVyN85LBEjgNRUT-jAEZYYyxHMV4wWMMYhPueGCdlG0gYBAxC3HaXJxs456bAApAAEogSEALLoQcLBIY4MJXiKYoSs4bQACqLueBRGCAoi-nGAC+bTZoWsw1NyYK5Js0JdGAABylzXMQxLEFo7C5P0UikOwewwL4wwAAz5EoiTDO+CkafkLojKMGmTNuWR+lgaiZgMZAQmAACCS40CuxBrrAW6HsYJmXhGDIQJg67ZO4XK8qIzTvgAnAAbAAHApEWfleiQASw6kKSwADU8WjCwUoOKmPaiL53miBAhqecUkFZu5LDODQdZsLFgEmFyVhaPoZBUl+jJQIkGDLpmiT5J8EDsMMADMSmEv1rLYMMZGMEMrUMkRMYVTWAhgAxAC6QA + - title: Core + file: app-variation-input-task-button-multi-core.py + shinylive: https://shinylive.io/py/app/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuIB0IdBsxYATKBTJ04AgUKasYE1ABtiZFTQBGWbKvVt2LVWTn0FLCAFd62Q5dSyIAM0bEYLdgAsaEO-JEAQXQ8FgBJCFQrMnZQgHloqJjQgGU4dnZeCFDGOChCKQA3OBzKUThGUKsaJzRUAH1qlgBeFmqsKABzOHrnFWrRAAoBFlG2mgxfJPrreEYaQmGwAA8+fBY11fXCqH64ZoBWAEo8EbH2qej6sg4Aa3qtaLJSJa0yCDXQtZSARysoXKWGxaCqeNQAdxU2DWJzOo3axESVwoyzIS3YPxhpwgY3GGC8jEGsJx5wmqGGJNxozWAHUfCo4CwSPRor5OqEyF5GVJ4G1UOIKEYoBBRCxsIimcLPFIVCoWL4KIx8qxwTROSxOYyfOxnp0lTAMGs4Sxibj2uS1gAVLka6TyoyfPGIsjTFFotaEKyMXLka7SR2+TQQfaWxhWOBHU2kyaRK7sTTlQlrD7rNYAOWBoOIzhYxC07AqOykpHYjpgvmaAAZQkpls0AIyVpuhHZ7Q5NqPwibO6b6d1gPtYgRHJwCco5guMYqEy5kRDhWPJXNIucsBIu6KxTzpTKkedpDJZI6IY0AAR9iawajRuzIzTWgRY2t1+phxvHxmvRJPlKpkQwSoiu4GAFnAQz1gAnAAbAAHJWcH1iOv64ssLQsI2lYsAA1OhBwsAAVA4AHCqIwGAaIECDLOGCUZGxq4s4NDsmwqGtCYIFWFofbsES9FjFAyz4jQOqDMsoRaL47DNAAzNWYiUJkZDYM0obhkhVJjLkZBejijGdE4uLnmUFQYG675wDmnrepQZB+vA358aMuTKjQxQxq2NACj0KgSBUgyIY5LBaTpYi+TycAYF54U0cQ4JEiBZCMM44VLAApAAEogqUALJZSkLCpY4YBIWe7QSSK1x3A8TwvI8ZDPBA9SefeYBvCm6ljEZLluXAqLGaIlXsLcxocFwhDyROPz1K24Y8csx6BVA4JQGqhhjbwIEMnA5LHIFwWMDiyz4fhABMBmdc5BSuRFFnOHABRnpdRQ3cU5BUYuGBtR1owfm19SEJohC3A5yFjBi027LN71JBgyxEiVv5GSKJlmb+H4YiDGlOXA2kHdKhLgzN6QAekVgqGidEfFTdRocE5J1I0NChJO05IWAAC+AC6QA + +--- + +:::{#example} +::: + +:::{#relevant-functions} +::: + +## Details + +The input task button is a special kind of button that allows long computations to not block +the behaviour of the rest of the application. +It looks very similar to the `ui.input_action_button()`, but uses the `ui.input_task_button()` function instead. + +An input task button appears as a button and makes a computation asynchronously. + +Follow these steps to add an action button to your app, perform a task, capture the result, and display the result: + +1. Add `ui.input_task_button()` to the UI of your app to create an input task button. + Where you call this function will determine where the button will appear within the app's layout. + +2. Specify the `id` and `label` parameters of `ui.input_task_button()` to define the button's identifier and label. + +3. Use one of the `ui_output_*` functions to capture the computed result, here we are using `ui.output_text()`. + +The input task button component can trigger a computation within the `server()` function. + +There are 2 main parts of the input task button in the `server()` function, +a function that is called when the button is clicked (this is similar to the `ui.input_task_button()` component), +a separate function that defines any long computation you wish to perform asynchronously +(this is specific to making `ui.input_task_button()` making an asynchronous calculation). + +To create the function that is called when the button is clicked: + +1. Create a function that will be run when the `ui.input_task_button` is clicked. +2. Decorate this function with the `id` used in your ui.input_task_button(), i.e., `@reactive.event(input.)`, this will make sure the function will only run when the button is clicked. +3. Add a second decorator, on top, `@reactive.effect` +4. In the body of the button click function, make a call to another function that you will use to make the long computation. + +To create the separate function that makes the asynchronous computation: + +1. Import the built-in Python `asyncio` module +2. Define a function that will be called in the button click step using the `async` and `await` syntax + See the [coroutines and tasks](https://docs.python.org/3/library/asyncio-task.html) official Python documentation + for details. +3. Return the calculated result in the function. + +To learn more, you can read this article about +[non-blocking operations](https://shiny.posit.co/py/docs/nonblocking.html). From bc2a1fb9f64e9a07c426ebb3d1ad741aa74697aa Mon Sep 17 00:00:00 2001 From: Daniel Chen Date: Thu, 5 Sep 2024 23:20:42 -0700 Subject: [PATCH 2/3] update component doc to use sklearn model + re-arrange details --- .../inputs/input-task-button/app-core.py | 45 ++++---- .../input-task-button/app-detail-preview.py | 41 +++---- .../inputs/input-task-button/app-express.py | 37 ++----- .../inputs/input-task-button/app-preview.py | 2 +- .../app-variation-extended-task-core.py | 92 ++++++++++++++++ .../app-variation-extended-task-express.py | 90 +++++++++++++++ ...-variation-input-task-button-multi-core.py | 53 --------- ...riation-input-task-button-multi-express.py | 53 --------- components/inputs/input-task-button/index.qmd | 104 ++++++++++++------ requirements.txt | 2 + 10 files changed, 305 insertions(+), 214 deletions(-) create mode 100644 components/inputs/input-task-button/app-variation-extended-task-core.py create mode 100644 components/inputs/input-task-button/app-variation-extended-task-express.py delete mode 100644 components/inputs/input-task-button/app-variation-input-task-button-multi-core.py delete mode 100644 components/inputs/input-task-button/app-variation-input-task-button-multi-express.py diff --git a/components/inputs/input-task-button/app-core.py b/components/inputs/input-task-button/app-core.py index 0fb23aee..93f46d32 100644 --- a/components/inputs/input-task-button/app-core.py +++ b/components/inputs/input-task-button/app-core.py @@ -1,37 +1,34 @@ -import asyncio # << -import datetime +## file: app.py +from time import sleep -from shiny import App, Inputs, Outputs, Session, reactive, render, ui +from shiny import App, reactive, render, ui app_ui = ui.page_fluid( - ui.p("The time is ", ui.output_text("current_time", inline=True)), - ui.hr(), - ui.input_task_button("btn", "Square 5 slowly"), # << - ui.output_text("sq"), + ui.row( + ui.column( + 6, + ui.input_task_button( # << + "task_button", # << + "Increase Number slowly", # << + ), # << + ), + ui.column(6, ui.output_text("counter")), + ) ) -def server(input: Inputs, output: Outputs, session: Session): - - @render.text - def current_time(): - reactive.invalidate_later(1) - return datetime.datetime.now().strftime("%H:%M:%S %p") - - @ui.bind_task_button(button_id="btn") # << - @reactive.extended_task # << - async def sq_value(x): # << - await asyncio.sleep(2) # << - return x**2 # << +def server(input, output, session): + count = reactive.value(0) @reactive.effect # << - @reactive.event(input.btn) # << - def btn_click(): - sq_value(5) # << + @reactive.event(input.task_button) # << + def _(): + sleep(1) + count.set(count() + 1) @render.text - def sq(): - return f"5 squared is: {str(sq_value.result())}" + def counter(): + return f"{count()}" app = App(app_ui, server) diff --git a/components/inputs/input-task-button/app-detail-preview.py b/components/inputs/input-task-button/app-detail-preview.py index 6ba98bf8..9ac1f778 100644 --- a/components/inputs/input-task-button/app-detail-preview.py +++ b/components/inputs/input-task-button/app-detail-preview.py @@ -1,37 +1,28 @@ -import asyncio -import datetime - -from shiny import App, Inputs, Outputs, Session, reactive, render, ui +## file: app.py +from time import sleep +from shiny import App, reactive, render, ui app_ui = ui.page_fluid( - ui.p("The time is ", ui.output_text("current_time", inline=True)), - ui.hr(), - ui.input_task_button("btn", "Square 5 slowly"), - ui.output_text("sq"), + ui.row( + ui.column(6, ui.input_task_button("task_button", "Increase Number slowly")), + ui.column(6, ui.output_text("counter").add_class("display-5 mb-0")), + {"class": "vh-100 justify-content-center align-items-center px-5"}, + ).add_class("text-center") ) -def server(input: Inputs, output: Outputs, session: Session): - - @render.text - def current_time(): - reactive.invalidate_later(1) - return datetime.datetime.now().strftime("%H:%M:%S %p") - - @ui.bind_task_button(button_id="btn") - @reactive.extended_task - async def sq_value(x): - await asyncio.sleep(2) - return x**2 +def server(input, output, session): + count = reactive.value(0) @reactive.effect - @reactive.event(input.btn) - def btn_click(): - sq_value(5) + @reactive.event(input.task_button) + def _(): + sleep(1) + count.set(count() + 1) @render.text - def sq(): - return f"5 squared is: {str(sq_value.result())}" + def counter(): + return f"{count()}" app = App(app_ui, server) diff --git a/components/inputs/input-task-button/app-express.py b/components/inputs/input-task-button/app-express.py index 123ff445..065d87b3 100644 --- a/components/inputs/input-task-button/app-express.py +++ b/components/inputs/input-task-button/app-express.py @@ -1,35 +1,22 @@ -import asyncio # << -import datetime +from time import sleep from shiny import reactive -from shiny.express import render, input, ui +from shiny.express import input, render, ui - -@render.text -def current_time(): - reactive.invalidate_later(1) - time_str = datetime.datetime.now().strftime("%H:%M:%S %p") - return f"The time is, {time_str}" +with ui.sidebar(): + ui.input_task_button("task_button", "Increase Number slowly") # << -ui.hr() - -ui.input_task_button("btn", "Square 5 slowly") # << +@render.text +def counter(): + return f"{count()}" -@ui.bind_task_button(button_id="btn") # << -@reactive.extended_task # << -async def sq_values(x): # << - await asyncio.sleep(2) # << - return x**2 # << +count = reactive.value(0) @reactive.effect # << -@reactive.event(input.btn) # << -def btn_click(): # << - sq_values(5) # << - - -@render.text -def sq(): - return str(sq_values.result()) +@reactive.event(input.task_button) # << +def _(): + sleep(1) + count.set(count() + 1) diff --git a/components/inputs/input-task-button/app-preview.py b/components/inputs/input-task-button/app-preview.py index c5719d61..44f9ffe4 100644 --- a/components/inputs/input-task-button/app-preview.py +++ b/components/inputs/input-task-button/app-preview.py @@ -2,7 +2,7 @@ from shiny import App, Inputs, Outputs, Session, reactive, ui app_ui = ui.page_fluid( - ui.input_task_button("btn", "Add numbers slowly"), + ui.input_task_button("btn", "Fit Model"), ) diff --git a/components/inputs/input-task-button/app-variation-extended-task-core.py b/components/inputs/input-task-button/app-variation-extended-task-core.py new file mode 100644 index 00000000..da986c76 --- /dev/null +++ b/components/inputs/input-task-button/app-variation-extended-task-core.py @@ -0,0 +1,92 @@ +import asyncio # << +from typing import List + +import seaborn as sns +from shiny import App, Inputs, Outputs, Session, reactive, render, ui +from sklearn.compose import ColumnTransformer +from sklearn.linear_model import LinearRegression +from sklearn.metrics import mean_squared_error +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import OneHotEncoder + +diamonds = sns.load_dataset("diamonds") + +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_selectize( + "predictors", + "Choose predictors", + ["carat", "color", "cut", "clarity"], + selected="carat", + multiple=True, + ), + ui.input_task_button("btn", "Fit Model"), + ui.input_switch("show", "Show Data Sample", True), + ), + ui.value_box("Mean Square Error", ui.output_ui("mse"), max_height=125), + ui.output_data_frame("diamonds_df"), +) + + +def server(input: Inputs, output: Outputs, session: Session): + @ui.bind_task_button(button_id="btn") # << + @reactive.extended_task # << + async def calc_model_mse(colnames: List[str]) -> float: # << + await asyncio.sleep(5) + + predictors = diamonds[colnames] + response = diamonds["price"] + + selected_cat_cols = [ + x for x in colnames if x in ["color", "cut", "clarity"] + ] + + # categorical variables selected one-hot encode / dummy variable encode + if selected_cat_cols: + categorical_transformer = Pipeline( + steps=[("encoder", OneHotEncoder(drop="first"))] + ) + preprocessor = ColumnTransformer( + transformers=[ + ("cat", categorical_transformer, selected_cat_cols), + ] + ) + steps = [ + ("preprocessor", preprocessor), + ("regressor", LinearRegression()), + ] + + # no categorical column selected, so no one-hot/dummy encoding needed + # we are doing this so lr is always a Pipeline object + else: + steps = [ + ("regressor", LinearRegression()), + ] + + # create pipeline + lr = Pipeline(steps=steps) + + # fit the model + lr = lr.fit(X=predictors, y=response) + + # predict on itself + pred = lr.predict(predictors) + + return mean_squared_error(predictors, pred) + + @reactive.effect # << + @reactive.event(input.btn) # << + def btn_click(): # << + calc_model_mse(list(input["predictors"]())) # << + + @render.text + def mse(): + return f"{calc_model_mse.result():.2f}" + + @render.data_frame + def diamonds_df(): + if input["show"](): + return render.DataTable(diamonds.sample(5)) + + +app = App(app_ui, server) diff --git a/components/inputs/input-task-button/app-variation-extended-task-express.py b/components/inputs/input-task-button/app-variation-extended-task-express.py new file mode 100644 index 00000000..2e858628 --- /dev/null +++ b/components/inputs/input-task-button/app-variation-extended-task-express.py @@ -0,0 +1,90 @@ +import asyncio # << +from typing import List + +import seaborn as sns +from shiny import reactive +from shiny.express import input, render, ui +from sklearn.compose import ColumnTransformer +from sklearn.linear_model import LinearRegression +from sklearn.metrics import mean_squared_error +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import OneHotEncoder + +diamonds = sns.load_dataset("diamonds") + + +with ui.sidebar(): + ui.input_selectize( + "predictors", + "Choose predictors", + ["carat", "color", "cut", "clarity"], + selected="carat", + multiple=True, + ) + ui.input_task_button("btn", "Fit Model") # << + ui.input_switch("show", "Show Data Sample", True) + +with ui.value_box(max_height=125): + "Mean Square Error" + + @render.ui + def mse(): + return f"{calc_model_mse.result():.2f}" + + +@render.data_frame +def diamonds_df(): + if input["show"](): + return render.DataTable(diamonds.sample(5)) + + +@ui.bind_task_button(button_id="btn") # << +@reactive.extended_task # << +async def calc_model_mse(colnames: List[str]) -> float: # << + await asyncio.sleep(5) + + predictors = diamonds[colnames] + response = diamonds["price"] + + selected_cat_cols = [ + x for x in colnames if x in ["color", "cut", "clarity"] + ] + + # categorical variables selected one-hot encode / dummy variable encode + if selected_cat_cols: + categorical_transformer = Pipeline( + steps=[("encoder", OneHotEncoder(drop="first"))] + ) + preprocessor = ColumnTransformer( + transformers=[ + ("cat", categorical_transformer, selected_cat_cols), + ] + ) + steps = [ + ("preprocessor", preprocessor), + ("regressor", LinearRegression()), + ] + + # no categorical column selected, so no one-hot/dummy encoding needed + # we are doing this so lr is always a Pipeline object + else: + steps = [ + ("regressor", LinearRegression()), + ] + + # create pipeline + lr = Pipeline(steps=steps) + + # fit the model + lr = lr.fit(X=predictors, y=response) + + # predict on itself + pred = lr.predict(predictors) + + return mean_squared_error(predictors, pred) + + +@reactive.effect # << +@reactive.event(input.btn) # << +def btn_click(): # << + calc_model_mse(list(input["predictors"]())) # << diff --git a/components/inputs/input-task-button/app-variation-input-task-button-multi-core.py b/components/inputs/input-task-button/app-variation-input-task-button-multi-core.py deleted file mode 100644 index 9f7d99da..00000000 --- a/components/inputs/input-task-button/app-variation-input-task-button-multi-core.py +++ /dev/null @@ -1,53 +0,0 @@ -import asyncio -import datetime - -import matplotlib.pyplot as plt -import numpy as np - -from shiny import App, Inputs, Outputs, Session, reactive, render, ui - -app_ui = ui.page_fluid( - ui.input_numeric("x", "x", value=5), - ui.input_task_button("btn", "Square number slowly"), - ui.output_text("sq"), - ui.hr(), - ui.p( - "While computing, the time updates and you can still interact with the histogram." - ), - ui.p("The time is ", ui.output_text("current_time", inline=True)), - ui.input_slider("n", "Number of observations", min=0, max=1000, value=500), - ui.output_plot("plot"), -) - - -def server(input: Inputs, output: Outputs, session: Session): - @render.plot(alt="A histogram") - def plot(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(input.n()) - fig, ax = plt.subplots() - ax.hist(x, bins=30, density=True) - return fig - - @render.text - def current_time(): - reactive.invalidate_later(1) - return datetime.datetime.now().strftime("%H:%M:%S %p") - - @ui.bind_task_button(button_id="btn") - @reactive.extended_task - async def sq_values(x): - await asyncio.sleep(5) - return x**2 - - @reactive.effect - @reactive.event(input.btn) - def btn_click(): - sq_values(input.x()) - - @render.text - def sq(): - return str(sq_values.result()) - - -app = App(app_ui, server) diff --git a/components/inputs/input-task-button/app-variation-input-task-button-multi-express.py b/components/inputs/input-task-button/app-variation-input-task-button-multi-express.py deleted file mode 100644 index 5fa1314f..00000000 --- a/components/inputs/input-task-button/app-variation-input-task-button-multi-express.py +++ /dev/null @@ -1,53 +0,0 @@ -import asyncio -import datetime - -import matplotlib.pyplot as plt -import numpy as np - -from shiny import reactive -from shiny.express import render, input, ui - -ui.input_numeric("x", "x", value=5), -ui.input_task_button("btn", "Square number slowly") - - -@ui.bind_task_button(button_id="btn") -@reactive.extended_task -async def sq_values(x): - await asyncio.sleep(5) - return x**2 - - -@reactive.effect -@reactive.event(input.btn) -def btn_click(): - sq_values(input.x()) - - -@render.text -def sq(): - return str(sq_values.result()) - - -ui.hr() - -ui.p("While computing, the time updates and you can still interact with the histogram.") - - -@render.text -def current_time(): - reactive.invalidate_later(1) - dt_str = datetime.datetime.now().strftime("%H:%M:%S %p") - return f"The time is {dt_str}" - - -ui.input_slider("n", "Number of observations", min=0, max=1000, value=500), - - -@render.plot(alt="A histogram") -def plot(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(input.n()) - fig, ax = plt.subplots() - ax.hist(x, bins=30, density=True) - return fig diff --git a/components/inputs/input-task-button/index.qmd b/components/inputs/input-task-button/index.qmd index ff3375cd..13ee8f15 100644 --- a/components/inputs/input-task-button/index.qmd +++ b/components/inputs/input-task-button/index.qmd @@ -1,5 +1,5 @@ --- -title: Input Task Button +title: Task Button sidebar: components appPreview: file: components/inputs/input-task-button/app-preview.py @@ -38,19 +38,20 @@ listing: template-params: dir: components/inputs/input-task-button/ contents: - - title: Input task button with inputs and interactivity - description: Here's an application that shows how the input task button can perform non-blocking computations. - It will take in 2 inputs to add together in a slow computation and you will will be able to move the slider - to make histogram modifications and see the time increment during the computation time. + - title: "Non-blocking task: fiting a scikit-learn model" + description: This example combines the task button with an extended task to keep the application responsive (i.e., not blocked) while the model is fitting. + In this example, the model fitting and MSE calculation is delayed when the task button is clicked to triggeer an extended task. + But, you are able to still toggle the data sampling while the extended task is running. + The MSE that is calcualted is from the initial set of predictors selected, even if you change the selection during the extended task. apps: - title: Preview - file: app-variation-input-task-button-multi-core.py + file: app-variation-extended-task-core.py - title: Express - file: app-variation-input-task-button-multi-express.py - shinylive: https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuIB0IdBsxYATKBTJ04AgUKasYE1ABtiZFTQBGWbKvVt2LVWTn0FLCAFd62Q5dSyIAM0bEYLdgAsaEO-JFGOChCKQA3GRc3D29fbAw4AA9UIPYjANYgiFE4RjwWX1QrMnyrGicyjELigH1reEYaQgAKPjBEtvy2jvwWMKgVKzgAXgBWAEo8AUrqshqyDgBrGq1islJWsC0yCE6WNoBlAEcrKCDLGy1czzUAdxVsNvGnAQABSq1fUXmllbWN1ZkdYQGo0UTDNrbXZgZ4QV5BELhOAJRIUbJwb4LdiLAQcLiEMRwZyeI41fqDODsZqJcaIAQsBlsW5QGisPHcXgYdgqOBwVDNCb0xlBMhWRgQFiJABUUoATC84QjQjQIglnM44KE3kqkQkIuRmrMMFDYTliVCaoRNIRFs1aUKGexSeShlSjYk7bCnPDKDlGBgKKiBGaSXa6RLhXBReLPGRGM0nWSBq6MKkrCoyJ6FZUvPGvdYaFhNgB1Hw8lgkejFXwAc3yZC8cBYUngLCsqHEFCMUGyLGwxCsFZ7sZoKhUBXIuURLFurK8zcbLB87HWNcYsAwTwVPvR-sDpnRxMIYqycxbcDDDpYOpVyN85LBEjgNRUT-jAEZYYyxHMV4wWMMYhPueGCdlG0gYBAxC3HaXJxs456bAApAAEogSEALLoQcLBIY4MJXiKYoSs4bQACqLueBRGCAoi-nGAC+bTZoWsw1NyYK5Js0JdGAABylzXMQxLEFo7C5P0UikOwewwL4wwAAz5EoiTDO+CkafkLojKMGmTNuWR+lgaiZgMZAQmAACCS40CuxBrrAW6HsYJmXhGDIQJg67ZO4XK8qIzTvgAnAAbAAHApEWfleiQASw6kKSwADU8WjCwUoOKmPaiL53miBAhqecUkFZu5LDODQdZsLFgEmFyVhaPoZBUl+jJQIkGDLpmiT5J8EDsMMADMSmEv1rLYMMZGMEMrUMkRMYVTWAhgAxAC6QA + file: app-variation-extended-task-express.py + shinylive: - title: Core - file: app-variation-input-task-button-multi-core.py - shinylive: https://shinylive.io/py/app/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuIB0IdBsxYATKBTJ04AgUKasYE1ABtiZFTQBGWbKvVt2LVWTn0FLCAFd62Q5dSyIAM0bEYLdgAsaEO-JEAQXQ8FgBJCFQrMnZQgHloqJjQgGU4dnZeCFDGOChCKQA3OBzKUThGUKsaJzRUAH1qlgBeFmqsKABzOHrnFWrRAAoBFlG2mgxfJPrreEYaQmGwAA8+fBY11fXCqH64ZoBWAEo8EbH2qej6sg4Aa3qtaLJSJa0yCDXQtZSARysoXKWGxaCqeNQAdxU2DWJzOo3axESVwoyzIS3YPxhpwgY3GGC8jEGsJx5wmqGGJNxozWAHUfCo4CwSPRor5OqEyF5GVJ4G1UOIKEYoBBRCxsIimcLPFIVCoWL4KIx8qxwTROSxOYyfOxnp0lTAMGs4Sxibj2uS1gAVLka6TyoyfPGIsjTFFotaEKyMXLka7SR2+TQQfaWxhWOBHU2kyaRK7sTTlQlrD7rNYAOWBoOIzhYxC07AqOykpHYjpgvmaAAZQkpls0AIyVpuhHZ7Q5NqPwibO6b6d1gPtYgRHJwCco5guMYqEy5kRDhWPJXNIucsBIu6KxTzpTKkedpDJZI6IY0AAR9iawajRuzIzTWgRY2t1+phxvHxmvRJPlKpkQwSoiu4GAFnAQz1gAnAAbAAHJWcH1iOv64ssLQsI2lYsAA1OhBwsAAVA4AHCqIwGAaIECDLOGCUZGxq4s4NDsmwqGtCYIFWFofbsES9FjFAyz4jQOqDMsoRaL47DNAAzNWYiUJkZDYM0obhkhVJjLkZBejijGdE4uLnmUFQYG675wDmnrepQZB+vA358aMuTKjQxQxq2NACj0KgSBUgyIY5LBaTpYi+TycAYF54U0cQ4JEiBZCMM44VLAApAAEogqUALJZSkLCpY4YBIWe7QSSK1x3A8TwvI8ZDPBA9SefeYBvCm6ljEZLluXAqLGaIlXsLcxocFwhDyROPz1K24Y8csx6BVA4JQGqhhjbwIEMnA5LHIFwWMDiyz4fhABMBmdc5BSuRFFnOHABRnpdRQ3cU5BUYuGBtR1owfm19SEJohC3A5yFjBi027LN71JBgyxEiVv5GSKJlmb+H4YiDGlOXA2kHdKhLgzN6QAekVgqGidEfFTdRocE5J1I0NChJO05IWAAC+AC6QA + file: app-variation-extended-task-core.py + shinylive: --- @@ -62,42 +63,79 @@ listing: ## Details -The input task button is a special kind of button that allows long computations to not block +The task button is created with the `ui.input_task_button()` function, +and is a drop-in replacment for `ui.input_action_button()`. +The task button is a special kind of button that allows long computations to not block the behaviour of the rest of the application. -It looks very similar to the `ui.input_action_button()`, but uses the `ui.input_task_button()` function instead. +That is, you can still use the rest of your application while something works in the background (more on this later). -An input task button appears as a button and makes a computation asynchronously. - -Follow these steps to add an action button to your app, perform a task, capture the result, and display the result: +If used on its own, +`ui.input_task_button()` will update when clicking on it starts a long-running computation +to let users know that something is happening on the server. +Follow these steps to +add an action button to your app, +perform a task, +capture the result, +and display the result: 1. Add `ui.input_task_button()` to the UI of your app to create an input task button. Where you call this function will determine where the button will appear within the app's layout. -2. Specify the `id` and `label` parameters of `ui.input_task_button()` to define the button's identifier and label. +2. Specify the `id` and `label` parameters of `ui.input_task_button()` + to define the button's identifier and label. + +3. Use one of the `ui_output_*` functions to capture the computed result, + here we are using `ui.output_text()`. + +4. Create a function that will be run when the `ui.input_task_button` is clicked. + +5. Decorate this function with the `id` used in your ui.input_task_button(), i.e., + `@reactive.event(input.)`, + this will make sure the function will only run when the button is clicked. + +6. Add a second decorator, on top, `@reactive.effect` + +7. In the body of the button click function, make a call to another function that you will use to make the long computation. + +### Task Button with Extended Tasks + +`ui.input_task_button()` +is even more effective when paired with an extended task, `@reactive.extended_task`, +which allows your application to remain responsive while a slow task is taking place +(see the variation example below). -3. Use one of the `ui_output_*` functions to capture the computed result, here we are using `ui.output_text()`. +Extended tasks run asynchronously and are truly non-blocking. +The rest of your app can keep working while the extended task runs. +The new decorator functions we will need while combining +a task button with an extended task are: +`@reactive.extended_task` and `@ui.bind_task_button()`. +An async function becomes an extended task with +`@reactive.extended_task` decorator and +it can drive the button state with `@ui.bind_task_button("btn_id")`. -The input task button component can trigger a computation within the `server()` function. +You still have to react to the button click event, +just like with `ui.input_action_button()`, +with something like `@reactive.event(input.btn_id)`. -There are 2 main parts of the input task button in the `server()` function, -a function that is called when the button is clicked (this is similar to the `ui.input_task_button()` component), -a separate function that defines any long computation you wish to perform asynchronously -(this is specific to making `ui.input_task_button()` making an asynchronous calculation). +To incorporate an long running extended task function we can follow the following steps. -To create the function that is called when the button is clicked: +First we need to create a an `async` function that will run the slow computation: -1. Create a function that will be run when the `ui.input_task_button` is clicked. -2. Decorate this function with the `id` used in your ui.input_task_button(), i.e., `@reactive.event(input.)`, this will make sure the function will only run when the button is clicked. -3. Add a second decorator, on top, `@reactive.effect` -4. In the body of the button click function, make a call to another function that you will use to make the long computation. +1. Import the built-in Python `asyncio` module. +2. Define an `async` function that will contain the slow computation. + See the [coroutines and tasks](https://docs.python.org/3/library/asyncio-task.html) + official Python documentation for details. +3. Decorate the `async` function with the `@reactive.extended_task` decorator +4. Then decorate again, above, with the `@ui.bind_task_button()` decorator. + Remember to pass the button ID into this decorator. -To create the separate function that makes the asynchronous computation: +Finally, we need to connect the button click event to call the `async` function: -1. Import the built-in Python `asyncio` module -2. Define a function that will be called in the button click step using the `async` and `await` syntax - See the [coroutines and tasks](https://docs.python.org/3/library/asyncio-task.html) official Python documentation - for details. -3. Return the calculated result in the function. +1. Define a function that will be called when the button is clicked. +2. This function will call our `async` function that holds the long computation. +3. Decorate this function with the `@reactive.event()` decorator. + Rememver to pass in the button ID into this decorator. +4. Decorate again, above, with `@reactive.effect` To learn more, you can read this article about [non-blocking operations](https://shiny.posit.co/py/docs/nonblocking.html). diff --git a/requirements.txt b/requirements.txt index 3eae7c16..89429c4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,5 @@ shinywidgets ipyleaflet==0.17.4 favicons griffe<1.0.0 # FIXME remove after posit-dev/py-shiny#1636 +scikit-learn==1.5.1 +scipy From 5569f9449d7244075a4e8f99f8882ba3260d3aeb Mon Sep 17 00:00:00 2001 From: Daniel Chen Date: Mon, 9 Sep 2024 09:24:23 -0700 Subject: [PATCH 3/3] write text on a single line --- components/inputs/input-task-button/index.qmd | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/components/inputs/input-task-button/index.qmd b/components/inputs/input-task-button/index.qmd index 13ee8f15..bc434451 100644 --- a/components/inputs/input-task-button/index.qmd +++ b/components/inputs/input-task-button/index.qmd @@ -39,10 +39,7 @@ listing: dir: components/inputs/input-task-button/ contents: - title: "Non-blocking task: fiting a scikit-learn model" - description: This example combines the task button with an extended task to keep the application responsive (i.e., not blocked) while the model is fitting. - In this example, the model fitting and MSE calculation is delayed when the task button is clicked to triggeer an extended task. - But, you are able to still toggle the data sampling while the extended task is running. - The MSE that is calcualted is from the initial set of predictors selected, even if you change the selection during the extended task. + description: This example combines the task button with an extended task to keep the application responsive (i.e., not blocked) while the model is fitting. In this example, the model fitting and MSE calculation is delayed when the task button is clicked to triggeer an extended task.But, you are able to still toggle the data sampling while the extended task is running. The MSE that is calcualted is from the initial set of predictors selected, even if you change the selection during the extended task. apps: - title: Preview file: app-variation-extended-task-core.py