From fa2c1268d5f8d4bc5ddfc0d4208e97c152ffdbf9 Mon Sep 17 00:00:00 2001 From: John Duggan Date: Mon, 10 Feb 2025 13:31:52 -0500 Subject: [PATCH 1/4] Add copier answers for episode 7 --- episodes/07-Advanced-Visualizations.md | 65 ++++++++++++++++++++------ 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/episodes/07-Advanced-Visualizations.md b/episodes/07-Advanced-Visualizations.md index 4baeda04..35a4e4e0 100755 --- a/episodes/07-Advanced-Visualizations.md +++ b/episodes/07-Advanced-Visualizations.md @@ -39,6 +39,41 @@ Let\'s start by setting up a new application from the template. When answering t ```bash copier copy https://code.ornl.gov/ndip/project-templates/nova-application-template.git viz_tutorial +``` + +* **What is your project name?** + + > Enter `Viz Examples` + +* **What is your Python package name (use Python naming conventions)?** + + > Press enter to accept the default. + +* **Do you want to install Mantid for your project?** + + > Enter `no` + +* ** Are you developing a GUI application using MVVM pattern?** + + > Enter `yes` + +* ** Which library will you use?** + + > Select `Trame` + +* **Do you want a template with multiple tabs? + + > Enter `yes` + +* **Publish to PyPI?** + + > Enter `no` + +* **Publish documentation to readthedocs.io?** + + > Enter `no` + +```bash cd viz_tutorial poetry install poetry run app @@ -56,7 +91,7 @@ The pandas install is only necessary for loading example data from Plotly, which Now, we can create a view that displays a Plotly figure. -**1. `PlotlyView` View Class (`src/nova_tutorial/views/plotly.py`):** +**1. `PlotlyView` View Class (`src/viz_examples/views/plotly.py`):** * **Imports**: Pay special attention to the plotly import. This module contains a Trame widget that will allow us to quickly add a Plotly chart to our view. @@ -116,7 +151,7 @@ class PlotlyView: As with our previous examples, there is a corresponding model. -**2. `PlotlyConfig` Model Class (src/nova_tutorial/models/plotly.py):** +**2. `PlotlyConfig` Model Class (src/viz_examples/models/plotly.py):** * **Imports**: The graph_objects module is how we will define the content for our chart. The iris module defines an example dataset. @@ -183,7 +218,7 @@ class PlotlyConfig(BaseModel): First, let's add replace the sample tabs from the template with the following: -**3. `src/nova_tutorial/views/tab_content_panel.py` (Modify):** +**3. `src/viz_examples/views/tab_content_panel.py` (Modify):** * **Import `PlotlyView`** @@ -207,7 +242,7 @@ And add the corresponding import: We also need to update the tabs to show an option for the Plotly view. -**4. `src/nova_tutorial/views/tabs_panel.py` (Modify):** +**4. `src/viz_examples/views/tabs_panel.py` (Modify):** ```python def create_ui(self) -> None: @@ -217,7 +252,7 @@ We also need to update the tabs to show an option for the Plotly view. Finally, we'll need to update the view model to bind our new classes. -**5. `src/nova_tutorial/view_models/main.py` (Modify):** +**5. `src/viz_examples/view_models/main.py` (Modify):** * **Import `PlotlyConfig`** @@ -262,7 +297,7 @@ PyVista contains built-in Trame support, but we still need to install the Trame Now we can set up our view. -**6. `PyVistaView` View Class (`src/nova_tutorial/views/pyvista.py`):** +**6. `PyVistaView` View Class (`src/viz_examples/views/pyvista.py`):** * **Imports:** `plotter_ui` contains the Trame widget for PyVista. @@ -320,7 +355,7 @@ class PyVistaView: self.view_model.update_pyvista_volume(self.plotter) ``` -**7. `PyVistaConfig` Model Class (`src/nova_tutorial/models/pyvista.py`):** +**7. `PyVistaConfig` Model Class (`src/viz_examples/models/pyvista.py`):** * **Imports:** `download_knee_full` yields a 3D dataset that is suitable for volume rendering. You can find more datasets in PyVista\'s [Dataset Gallery](https://docs.pyvista.org/api/examples/dataset_gallery). @@ -368,7 +403,7 @@ PyVista\'s volume rendering engine isn\'t currently suitable for large data. If This is very similar to the Plotly setup. -**8. `src/nova_tutorial/views/tab_content_panel.py` (Modify):** +**8. `src/viz_examples/views/tab_content_panel.py` (Modify):** * **Import `PyVistaView`** @@ -390,7 +425,7 @@ from ..views.pyvista import PyVistaView PyVistaView(self.view_model) ``` -**9. `src/nova_tutorial/views/tabs_panel.py` (Modify):** +**9. `src/viz_examples/views/tabs_panel.py` (Modify):** ```python def create_ui(self) -> None: @@ -399,7 +434,7 @@ from ..views.pyvista import PyVistaView vuetify.VTab("PyVista", value=2) ``` -**10. `src/nova_tutorial/view_models/main.py` (Modify):** +**10. `src/viz_examples/view_models/main.py` (Modify):** * **Import `PyVistaConfig`** @@ -448,7 +483,7 @@ PyVista isn't compatible with VTK 9.4, yet. If you are not using PyVista, there Once more, let's setup a view and model. -**11. `VTKView` View Class (`src/nova_tutorial/views/vtk.py`):** +**11. `VTKView` View Class (`src/viz_examples/views/vtk.py`):** * **Imports:** The `vtkRenderingVolumeOpenGL2` import is necessary despite being unreferenced. @@ -509,7 +544,7 @@ class VTKView: self.render_window.Render() ``` -**12. `VTKConfig` Model Class (`src/nova_tutorial/models/vtk.py`):** +**12. `VTKConfig` Model Class (`src/viz_examples/models/vtk.py`):** * **Imports:** We are only using PyVista to get an example dataset. There are two references to it as we use `KNEE_DATA` to compute min/max bounds for the data and `KNEE_DATAFILE` to pass the data file into a VTK reader. The FixedPointVolumeRayCastMapper is CPU-based, but other mappers are available if you need GPU support. @@ -641,7 +676,7 @@ class VTKConfig: This is very similar to the Plotly and PyVista setup. -**13. `src/nova_tutorial/views/tab_content_panel.py` (Modify):** +**13. `src/viz_examples/views/tab_content_panel.py` (Modify):** * **Import `VTKView`** @@ -665,7 +700,7 @@ from ..views.vtk import VTKView VTKView(self.view_model) ``` -**14. `src/nova_tutorial/views/tabs_panel.py` (Modify):** +**14. `src/viz_examples/views/tabs_panel.py` (Modify):** ```python def create_ui(self) -> None: @@ -675,7 +710,7 @@ from ..views.vtk import VTKView vuetify.VTab("VTK", value=3) ``` -**15. `src/nova_tutorial/view_models/main.py` (Modify):** +**15. `src/viz_examples/view_models/main.py` (Modify):** * **Import `VTKConfig`** -- GitLab From 9351838da50f742a2d99f08730d7580214963682 Mon Sep 17 00:00:00 2001 From: John Duggan Date: Mon, 10 Feb 2025 14:12:52 -0500 Subject: [PATCH 2/4] Fix run command --- episodes/05-Working-with-Trame.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/episodes/05-Working-with-Trame.md b/episodes/05-Working-with-Trame.md index b16fa674..fc6e22aa 100755 --- a/episodes/05-Working-with-Trame.md +++ b/episodes/05-Working-with-Trame.md @@ -269,7 +269,7 @@ In `SampleTab1`, we\'ve used a `VBoxLayout` to vertically stack the `InputField` To run the code, use the following command in the top level of your `nova_tutorial` project: ```bash -poetry run start +poetry run app ``` You should now see the simple UI. When you click the "Sample Tab 1" and "Sample Tab 2" tabs, you should now see the updated content with the new UI components. -- GitLab From d639430f2f39890d4277dc4ff44bd39bc095302a Mon Sep 17 00:00:00 2001 From: John Duggan Date: Mon, 10 Feb 2025 14:13:21 -0500 Subject: [PATCH 3/4] Use local slot diagram to fix image hotload failures --- episodes/05-Working-with-Trame.md | 2 +- episodes/fig/layout.png | Bin 0 -> 6874 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100755 episodes/fig/layout.png diff --git a/episodes/05-Working-with-Trame.md b/episodes/05-Working-with-Trame.md index fc6e22aa..1ff058f9 100755 --- a/episodes/05-Working-with-Trame.md +++ b/episodes/05-Working-with-Trame.md @@ -121,7 +121,7 @@ The `with` syntax is used by Trame to add content to a slot. This allows your vi Here is a layout diagram showing all of the available slots in `ThemedApp`: -![The `nova-trame` slot diagram for its default layout](https://nova-application-development.readthedocs.io/projects/nova-trame/en/stable/_images/layout.png) +![The `nova-trame` slot diagram for its default layout](fig/layout.png) ::::::::::::::::::::::::::::::::::::::::: callout diff --git a/episodes/fig/layout.png b/episodes/fig/layout.png new file mode 100755 index 0000000000000000000000000000000000000000..f3464064ea39f29cb63f68031f685ced9dd50672 GIT binary patch literal 6874 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 x;#w#pONEYZ{^eU04eb{{v0 Date: Mon, 10 Feb 2025 15:33:45 -0500 Subject: [PATCH 4/4] Add example for displaying a spinner during a long-running job --- .../nova_tutorial/app/models/main_model.py | 2 +- .../src/nova_tutorial/app/view_models/main.py | 22 ++++ .../nova_tutorial/app/views/fractal_tab.py | 23 ++-- episodes/05-Working-with-Trame.md | 101 +++++++++++++++++- 4 files changed, 130 insertions(+), 18 deletions(-) diff --git a/code/episode_5/src/nova_tutorial/app/models/main_model.py b/code/episode_5/src/nova_tutorial/app/models/main_model.py index 15e6567a..1a08559c 100755 --- a/code/episode_5/src/nova_tutorial/app/models/main_model.py +++ b/code/episode_5/src/nova_tutorial/app/models/main_model.py @@ -22,5 +22,5 @@ class MainModel(BaseModel): examples=["user"], ) password: str = Field(default="test_password", title="User Password") - fractal: Fractal = Field(default_factory=Fractal) file: str = Field(default="", title="Select a File") + fractal: Fractal = Field(default_factory=Fractal) diff --git a/code/episode_5/src/nova_tutorial/app/view_models/main.py b/code/episode_5/src/nova_tutorial/app/view_models/main.py index 1bceca83..c554a222 100755 --- a/code/episode_5/src/nova_tutorial/app/view_models/main.py +++ b/code/episode_5/src/nova_tutorial/app/view_models/main.py @@ -1,5 +1,7 @@ """Module for the main ViewModel.""" +from asyncio import create_task, sleep +from threading import Thread from typing import Any, Dict from nova.mvvm.interface import BindingInterface @@ -12,6 +14,7 @@ class MainViewModel: def __init__(self, model: MainModel, binding: BindingInterface): self.model = model + self.running = False # here we create a bind that connects ViewModel with View. It returns a communicator object, # that allows to update View from ViewModel (by calling update_view). @@ -19,6 +22,7 @@ class MainViewModel: # but one also can provide a callback function if they want to react to those events # and/or process errors. self.config_bind = binding.new_bind(self.model, callback_after_update=self.change_callback) + self.running_bind = binding.new_bind() def change_callback(self, results: Dict[str, Any]) -> None: if results["error"]: @@ -28,6 +32,24 @@ class MainViewModel: def update_view(self) -> None: self.config_bind.update_in_view(self.model) + self.running_bind.update_in_view(self.running) def run_fractal(self) -> None: + self.running = True + self.update_view() + + # update_view won't take effect until this method returns a value, so we must offload this long-running task to + # a background thread for our conditional rendering to work. + fractal_tool_thread = Thread(target=self.run_fractal_in_background, daemon=True) + fractal_tool_thread.start() + + create_task(self.monitor_fractal()) + + def run_fractal_in_background(self) -> None: self.model.fractal.run_fractal_tool() + self.running = False + + async def monitor_fractal(self) -> None: + while self.running: + await sleep(0.1) + self.update_view() diff --git a/code/episode_5/src/nova_tutorial/app/views/fractal_tab.py b/code/episode_5/src/nova_tutorial/app/views/fractal_tab.py index 465deb9b..b46f0f3b 100755 --- a/code/episode_5/src/nova_tutorial/app/views/fractal_tab.py +++ b/code/episode_5/src/nova_tutorial/app/views/fractal_tab.py @@ -2,24 +2,19 @@ from trame.widgets import vuetify3 as vuetify from nova.trame.view.components import InputField from nova_tutorial.app.view_models.main import MainViewModel -from nova.trame.view import layouts class FractalTab: - def __init__(self, view_model: MainViewModel) -> None: self.view_model = view_model + self.view_model.running_bind.connect("running") self.create_ui() def create_ui(self) -> None: - with layouts.VBoxLayout(classes="ma-4"): - with vuetify.VCard(classes="pa-4"): - InputField( - v_model=("config.fractal.fractal_type", "mandelbrot"), - label="Fractal Type", - ) - vuetify.VBtn( - "Run Fractal", - click=self.view_model.run_fractal, - classes="mt-2", - ) - vuetify.VCardText(v_text="config.status_message", classes="mt-2") \ No newline at end of file + InputField(v_model="config.fractal.fractal_type", classes="mb-2") + vuetify.VProgressCircular(v_if="running", indeterminate=True) + vuetify.VBtn( + "Run Fractal", + v_else=True, + click=self.view_model.run_fractal, # calls the run_fractal_tool method + ) + vuetify.VImg(src=("config.fractal.image_data",), height="400", width="400") diff --git a/episodes/05-Working-with-Trame.md b/episodes/05-Working-with-Trame.md index 1ff058f9..063d910d 100755 --- a/episodes/05-Working-with-Trame.md +++ b/episodes/05-Working-with-Trame.md @@ -222,7 +222,7 @@ We\'ll add an `InputField` and a `VBoxLayout` to this tab. ```python """Module for the Sample Tab 1.""" -from nova.trame.view.components import InputField +from nova.trame.view.components import InputField, RemoteFileInput from nova.trame.view import layouts from trame.widgets import vuetify3 as vuetify @@ -236,10 +236,27 @@ class SampleTab1: with layouts.VBoxLayout(classes="ma-2"): InputField(v_model="config.username", label="Username") vuetify.VCheckbox(label="Remember me") - RemoteFileInput(v_model="config.file", base_paths=["/SNS"]) + RemoteFileInput(v_model="config.file", base_paths=["/HFIR", "/SNS"]) ``` -**3. `nova_tutorial/app/views/sample_tab_2.py` (Modify):** +Since `config.file` doesn't exist yet, we\'ll need to add it to the model. + +**3. `nova_tutorial/app/models/main_model.py` (Modify):** + +```python + username: str = Field( + default="test_name", + min_length=1, + title="User Name", + description="Please provide the name of the user", + examples=["user"], + ) + password: str = Field(default="test_password", title="User Password") + file: str = Field(default="", title="Select a File") + fractal: Fractal = Field(default_factory=Fractal) +``` + +**4. `nova_tutorial/app/views/sample_tab_2.py` (Modify):** We\'ll add a `GridLayout` and an `InputField` to this tab. @@ -274,6 +291,84 @@ poetry run app You should now see the simple UI. When you click the "Sample Tab 1" and "Sample Tab 2" tabs, you should now see the updated content with the new UI components. +## Advanced Topics (Asynchronicity & Conditional Rendering) + +Now that we understand the basics of working with Trame, let\'s make the view for the fractal tab a bit more intuitive for the user by giving them a visual indicator that the job is running. + +**5. `nova_tutorial/app/views/fractal_tab.py` (Modify):** + +```python + def __init__(self, view_model: MainViewModel) -> None: + self.view_model = view_model + self.view_model.running_bind.connect("running") + self.create_ui() + + def create_ui(self) -> None: + InputField(v_model="config.fractal.fractal_type", classes="mb-2") + vuetify.VProgressCircular(v_if="running", indeterminate=True) + vuetify.VBtn( + "Run Fractal", + v_else=True, + click=self.view_model.run_fractal # calls the run_fractal_tool method + ) + vuetify.VImg(src=("config.fractal.image_data",), height="400", width="400") +``` + +We will need to add a data binding for `running`, as well. We choose to place this directly in the view model as this is not relevant to running the fractal tool on NDIP. + +**6. `nova_tutorial/app/view_models/main.py` (Modify):** + +```python + def __init__(self, model: MainModel, binding: BindingInterface): + self.model = model + self.running = False + + # here we create a bind that connects ViewModel with View. It returns a communicator object, + # that allows to update View from ViewModel (by calling update_view). + # self.model will be updated automatically on changes of connected fields in View, + # but one also can provide a callback function if they want to react to those events + # and/or process errors. + self.config_bind = binding.new_bind(self.model, callback_after_update=self.change_callback) + self.running_bind = binding.new_bind() + + def update_view(self) -> None: + self.config_bind.update_in_view(self.model) + self.running_bind.update_in_view(self.running) +``` + +Finally, we manipulate our new view state based on the current status of the tool. Because the fractal tool takes a long time to complete, we offload it to a background thread. If we do not do this, then Trame will not update the view until the tool has finished running, which defeats the purpose of this change. + +```python + def run_fractal(self) -> None: + self.running = True + self.update_view() + + # update_view won't take effect until this method returns a value, so we must offload this long-running task to + # a background thread for our conditional rendering to work. + fractal_tool_thread = Thread(target=self.run_fractal_in_background, daemon=True) + fractal_tool_thread.start() + + # We also need to know when the tool is done running so that we can + create_task(self.monitor_fractal()) + + def run_fractal_in_background(self) -> None: + self.model.fractal.run_fractal_tool() + self.running = False + + async def monitor_fractal(self) -> None: + while self.running: + await sleep(0.1) + self.update_view() +``` + +::::::::::::::::::::::::::::::::: callout +With any Trame or `nova-trame` component, you can use the `v_if`, `v_else_if`, and `v_else` arguments to only show the component in the interface when a condition is true. The condition can be a reference to your model, similar to the `v_model` argument, or it can be a full JavaScript expression for complex use cases. +::::::::::::::::::::::::::::::::::::::::: + +::::::::::::::::::::::::::::::::: callout +One major caveat when working with Trame is that Trame itself runs in the main thread of your application. Since Trame is responsible for syncing state between the server and the user interface, if you run a long, CPU-bound task in the main thread then Trame will freeze and your user interface will likely crash. If you need to run a long job (for example, a Mantid command that takes several minutes), then it is your responsibility to ensure that the task is run in a separate thread. +::::::::::::::::::::::::::::::::::::::::: + ::::::::::::::::::::::::::::::::::::::: challenge **Explore the `InputField` Component** Modify the `InputField` component in `SampleTab1` to automatically retrieve the label, hint, and validation rules from a Pydantic model field. Create a simple Pydantic model with a `username` field with a `title`, `description`, and `min_length` constraint. -- GitLab