Skip to main content

Component Applications

In this workshop, you will learn the fundamentals of Tethys Component App development by developing a simple, interactive well groundwater drawdown calculator application.

Objectives

  • Learn the fundamentals of component app development, including:
    • How to scaffold a new Tethys Component App
    • How to define an App class and App.page
    • How to build a component tree with inputs, plots, and layout
    • How to use reactive state and event handlers
    • How to connect model code to visualization
  • Create a simple, interactive well groundwater drawdown calculator application that uses the Theis solution and lets users explore how the model responds to adjusting various inputs.

Prerequisites

  • Complete the Introduction — you should have a tethys-workshop directory and a tethys virtual environment with Tethys Platform installed.
  • This tutorial additionally needs numpy, scipy, and reactpy-django. You will install these in the next section.

Create the App Project

This section shows how to create the workshop app and get the development environment ready.

Install the additional dependencies

Change into the tethys-workshop directory you created in the Introduction and activate your virtual environment:

Linux / macOS:

cd tethys-workshop # if you are not already there
source ./tethys/bin/activate

Windows:

cd tethys-workshop # if you are not already there
.\tethys\Scripts\activate

Your prompt should now start with (tethys). Install the additional Python packages this tutorial uses:

pip install numpy scipy reactpy-django

What each package is for:

  • numpy and scipy — scientific Python packages used by the well drawdown model
  • reactpy-django — the React bridge required by Tethys Component Apps

Scaffold the app

With the dependencies installed and your virtual environment still active, scaffold the app by running:

tethys scaffold well_drawdown_calculator -t component

You can accept the defaults for all of the prompted values.

Install the app

Install the app in development mode so changes reload automatically, migrate any db changes required by the app, and then start your server with the following commands:

cd tethysapp-well_drawdown_calculator
tethys install -d
tethys db migrate
tethys start

Open the app

Open your browser and navigate to:

http://localhost:8000/apps/well-drawdown-calculator

Open the code

Open the tethysapp-well_drawdown_calculator folder in your favorite IDE. The main file we will be editing is located here, relative to the core folder:

tethysapp/well_drawdown_calculator/app.py

The app.py file is a basic starting point for building any app. In it you will find an App class and a home function decorated with the @App.page decorator.


App Class and Page Structure

This section explains the main app class and the home page function.

App class

The App class is used to define the application metadata and core properties. It typically includes:

  • name
  • description
  • package
  • index
  • root_url

The index page for this app is the home page.

Page function

Page functions are one of the fundamental building blocks of Component App development. A page function is any function decorated with the @App.page decorator that accepts a lib argument and returns a component content tree that represents what will actually be rendered on your web page.

In the case of the boilerplate example from the scaffold, the default page is generated by the home function, which accepts the lib argument and then uses it to return a Display component containing a Map component.

@App.page
def home(lib):
return lib.tethys.Display(
lib.tethys.Map()
)

Note that each of these components are accessed under the tethys namespace, or submodule, of the lib object. More on the lib object and its submodules later.

Think of components like building blocks. Components can be very basic and single-purpose, such as an image (e.g. Image) or some text (e.g. Title). They can also be combined and contained within one another, as their design allows, to make more custom and complex components. For example, a CaptionedImage component could be created that combines both Image and Title into a single configuration.

In terms of Python syntax, components are simply Python functions. Whether simple or complex, components, like any Python function, almost always accept one or more arguments. In the case of components, these are either:

  • args that define its nested content in the form of one or more nested components, such as the Map component nested within the Display in the boilerplate code.
  • kwargs that define its visual properties, such as its rendered size and color.

Important: Although normal Python functions can directly accept both args and kwargs in that order (e.g. myfunc('prop1', prop4="i_skipped_a_few")), Tethys components can only directly accept one or the other (e.g. Component(*args_for_nested_components) or Component(**kwargs_for_properties)). If a component needs both, only the kwargs (i.e. properties) get passed directly to the component function, while the args (i.e. nested components) get passed to a second, chained call of the component function (e.g. Component(**kwargs_for_properties)(*args_for_nested_components)). This seems odd at first, but you will quickly get used to it.

Returning to our goals with this app, we will not be needing a Map for our example, so let's instead add a simple Title, like so:

return lib.tethys.Display(
lib.m.Title("Well Drawdown Calculator")
)

Note that in the case of the Title component, we are using lib.m instead of lib.tethys. The lib argument to page functions is an object that provides access to a handful of submodules - most of which are predefined libraries of components that can be used in your apps. Each submodule is accessed through its registered namespace, such as m or tethys.

Most of the submodules are actually based on the underlying third-party JavaScript React component libraries that they expose via a Pythonic API. For example, the m submodule exposes the Mantine Component Library. This is a main area in which Tethys Component App development thrives and shines, since the JavaScript React community has developed dozens of component libraries that provide almost everything you could ever need for web development. Many of these are exposed under various submodules (see Third-Party ReactJS Libraries for the full list).

It is also possible to register your own wrapper around an existing third-party ReactJS library if you find a component library out there that is not already part of the Tethys Component Library. Though we will not get into that for this workshop, you can see our official documentation on it here.

Though these third-party JavaScript React component library submodules are powerful, they also expose one of the few challenges with Tethys Component App development: reading and "translating" JavaScript documentation to its Pythonic equivalent. For a crash-course on that, see our official documentation here.

The tethys module provides access to custom, complex components created specifically for common Tethys app use cases, such as the Map and Chart. Under the covers, these tethys components are combining a handful of the third-party ReactJS components into a specific configuration of nested components and properties and controlling which args as nested componentsandkwargs` as properties can be provided by the user for further customization.

Now that you understand a bit more about the Tethys Component App Library (i.e. the lib object), you can try playing with the lib.m.Title we added by providing and tweaking various props_as_kwargs per the official documentation here. Note that the actual text content of the Title is a nested text component, rather than a property. For example, you could try something like this: lib.m.Title(order=2)("Well Drawdown Calculator").

Now we are ready to get to the main focus of our application - that which makes it unique: the well drawdown function.

Core model/algorithm function

Since the purpose of our application is to provide an interactive well drawdown calculator, the core backend processing logic of our app will be the execution of our well drawdown calculation function.

Remember that Tethys app development is tailored to geoscientific apps built on Python. While this workshop focuses on the well drawdown calculator, any complex model or algorithm written in Python could be similarly exposed and interacted with via a Tethys Application.

Since we're more focused on the app development aspect and not the geoscientific domain knowledge, we will just provide the code for the well drawdown calculation with little explanation. It is best practice to keep your business (i.e. geoscientific) logic separate from your web-application-specific logic, so create a model.py file and paste the following content inside.

import numpy as np
from scipy.special import exp1

def calculate_drawdown(pumping_rate, transmissivity, storativity, time_days, distances):
"""
Calculates groundwater drawdown using the Theis Equation.

Args:
pumping_rate (float): Q in m^3/day
transmissivity (float): T in m^2/day
storativity (float): S (dimensionless)
time_days (float): t in days
distances (np.array): radial distances from the well in meters

Returns:
np.array: Drawdown (s) in meters at each distance.
"""
# Safety check to avoid division by zero if time is 0
if time_days <= 0:
return np.zeros_like(distances)

# u = (r^2 * S) / (4 * T * t)
u = (distances**2 * storativity) / (4 * transmissivity * time_days)

# Theis equation: s = (Q / (4 * pi * T)) * W(u)
# exp1(u) is the Exponential Integral, mathematically equivalent to the Well Function W(u)
drawdown = (pumping_rate / (4 * np.pi * transmissivity)) * exp1(u)

return drawdown

The most important general note about exposing a model or algorithm via your application is that any and all specific variables that you want to expose to users for modification and/or submission need to be arguments and/or variables in your model or algorithm function. In this example we have five such variables that are all part of the calculate_drawdown function signature (i.e. arguments): pumping_rate, transmissivity, storativity, time_days, and distances.

Next, we will design and implement the components that will be displayed in our app to provide our users the ability to modify these algorithm variables.

Add State Variables

Since we know that we want users to be able to provide and adjust the values to the five arguments (i.e. variables) of our calculate_drawdown function, we need to create special web variables for these in our home page function. These special web variables are known as state variables.

Any components of your web page that must be dynamic based on conditional events, such as how a user interacts with the page, must have an accompanying state variable and its corresponding setter or updater function. The creation and management of these state variables are facilitated via the lib.hooks.use_state function. Note that the hooks namespace submodule provides this function.

The lib.hooks.use_state function:

  • Takes a single argument that represents the initial, default value the variable should hold. This is what should be shown when your application page first loads.
  • Returns two objects:
    • The state variable, which contains the current value of the variable, which at any given time could be different than the initial, default value provided to the function.
    • The setter/updater function that should be used elsewhere in your application logic when the state variable needs to be updated.

Go ahead and create a state variable for each of the five calculate_drawdown arguments. It will end up looking something like this:

@App.page
def home(lib):
# State management for reactive inputs
q, set_q = lib.hooks.use_state(1000) # Pumping rate (m3/day)
t, set_t = lib.hooks.use_state(100) # Transmissivity (m2/day)
s_exponent, set_s_exponent = lib.hooks.use_state(-4) # Storativity (unitless)
time, set_time = lib.hooks.use_state(10) # Time (days)
max_distance, set_max_distance = lib.hooks.use_state(100) # Max distance from well (m)
samples, set_samples = lib.hooks.use_state(100) # Number of sample points taken between well and max_distance

Note the following:

  • We used shortened variable names where it made sense (e.g. q rather than pumping_rate) to facilitate less keystrokes.
  • With foresight, we decided to have the user only provide the "exponent" of storativity, since acceptable values are between 10^-5 and 10^-3.
  • We also are only going to allow the user to pass in a single, max distance rather than an array of distances, since the array can easily be derived with the np.linspace function, as you'll see next.
  • With foresight, we added a samples variable that we will also let the user adjust to change the resolution of the graphs

Add transitionary calculations

Because our state variables are not the exact values needed for our calculate_drawdown function, we need to massage them a bit before using them in our function. There is a bit of foresight needed here, too, depending on what exactly we want our application page to look like.

In the case of well water drawdown, this is best visualized in either a x-y chart with distance vs. drawdown or a 2D heat map. We'd like to provide both, so that means we need to create both a single array representing sampled points along a single max_distance line from the well, and a 2d array representing sampled points in all radial directions of max_distance from the well.

We can then call the calculate_drawdown function once for each of these cases to get the two arrays we plan to plot.

First, we will need to import our calculate_drawdown function as well as numpy. Add these lines to the top of your app.py:

from .model import calculate_drawdown
import numpy as np

Next, add the following code just below the state variable declarations in your home function:

# Derive the actual Storativity value for the model
s_actual = 10 ** s_exponent

# Create a 2D grid of x and y coordinates (e.g., -500m to 500m)
x = np.linspace(-max_distance, max_distance, samples)
y = np.linspace(-max_distance, max_distance, samples)
X, Y = np.meshgrid(x, y)

# Calculate radial distance 'r' for every point in the grid
R = np.sqrt(X**2 + Y**2)

# Model execution (triggers every time state changes)
distances = np.linspace(1, max_distance, samples)
drawdown_line = calculate_drawdown(q, t, s_actual, time, distances)
drawdown_2d = calculate_drawdown(q, t, s_actual, time, R)

Now that we have everything that we need to display stored in either state variables or subsequently calculated standard variables, we can focus on building our component display. This means passing in new args as nested components to our lib.tethys.Display component.

Design the UI

Before we start writing code for our user interface, we should have at least a basic idea of what we want it to look like and how the user will interact with it.

For this application, let's go with the following:

┌────────────────────────────────┐
│ Title (full width) │
├────────────────────────────────┤
│ Input Controls (Full width) │
│ (Sliders with labels/values) │
├─────────────────┬──────────────┤
│ Line Plot │ Heatmap │
│ (half-width) │ (half-width) │
└─────────────────┴──────────────┘

This layout is effective because:

  • Inputs at top: Users immediately see what they can adjust
  • Side-by-side plots: Easy to compare the 1D vs 2D visualization

We can also ensure that each parameter has a dedicated color that appears in both the sliders and the values. This consistency can help users create mental associations that facilitate their experience. Let's go with the following:

  • Yellow = Max Distance
  • Turquoise = Samples
  • Blue = Pumping Rate
  • Green = Transmissivity
  • Purple = Storativity
  • Orange = Time

Now that we know what we want, we need to choose the best components with which to build it.

Implement UI Design

Layout

The overall layout structure can be achieved using Mantine's Grid System components, Grid and GridCol (see https://mantine.dev/core/grid/). The grid system is based on a 12-column layout, where each GridCol specifies how many columns it should span. Let's replace the Title component (which we'll re-insert in its proper place in a moment) with the following:

return lib.tethys.Display(
lib.m.Grid(
# Title on its own row (span=12)
lib.m.GridCol(span=12)(
# Title goes here
),
# All input controls on one row (span=12)
lib.m.GridCol(span=12)(
# Input controls go here
),
# Line plot on left (span=6)
lib.m.GridCol(span=6)(
# Plot goes here
),
# Heatmap on right (span=6)
lib.m.GridCol(span=6)(
# Plot goes here
),
)
)

This layout creates:

  • Two full-width columns (span=12) for title and input controls
  • Two side-by-side columns (span=6 each) for the plots

Title

The title is extremely simple and can be achieved with Mantine's Title component that we already explored a bit above. Nest it inside of the first GridCol component like so:

lib.m.GridCol(span=12)(
lib.m.Title(order=1)("Well Drawdown Calculator")
),

Input Controls

Now for each parameter, we'll use the following pattern:

lib.m.Group(
lib.m.Text(size="sm")("Max Distance from Well (m)"),
lib.m.Badge(color="yellow")(max_distance),
),
lib.m.Slider(
color="yellow",
value=max_distance,
min=50,
max=200,
step=5,
onChangeEnd=set_max_distance
),

This pattern includes:

Note that the value property of the Slider is set to the state variable we created for this parameter, max_distance, and that the setter for this variable is passed to the onChangeEnd to ensure that max_distance stays in-sync with the value that the user has chosen with the Slider. More on this concept later.

Go ahead and add all six parameter controls inside the first GridCol(span=12), like so:

lib.m.GridCol(span=12)(
# Max Distance
lib.m.Group(
lib.m.Text(size="sm")("Max Distance from Well (m)"),
lib.m.Badge(color="yellow")(max_distance),
),
lib.m.Slider(
color="yellow",
value=max_distance,
min=50,
max=200,
step=5,
onChangeEnd=set_max_distance
),

# Samples
lib.m.Group(
lib.m.Text(size="sm")("Samples"),
lib.m.Badge(color="turquoise")(samples),
),
lib.m.Slider(
color="turquoise",
value=samples,
min=10,
max=100,
step=5,
onChangeEnd=set_samples
),

# Pumping Rate
lib.m.Group(
lib.m.Text(size="sm")("Pumping Rate (m³/day)"),
lib.m.Badge(color="blue")(q),
),
lib.m.Slider(
color="blue",
value=q,
min=100,
max=5000,
step=100,
onChangeEnd=set_q
),

# Transmissivity
lib.m.Group(
lib.m.Text(size="sm")("Transmissivity (m²/day)"),
lib.m.Badge(color="green")(t),
),
lib.m.Slider(
color="green",
min=10,
max=1000,
value=t,
onChangeEnd=set_t
),

# Storativity
lib.m.Group(
lib.m.Text(size="sm")("Storativity (log10(S))"),
lib.m.Badge(color="purple")(f"{s_exponent:.2f}"),
),
lib.m.Slider(
color="purple",
min=-5,
max=-1,
step=0.1,
value=s_exponent,
onChangeEnd=set_s_exponent
),

# Time
lib.m.Group(
lib.m.Text(size="sm")("Time (days)"),
lib.m.Badge(color="orange")(time),
),
lib.m.Slider(
color="orange",
min=1,
max=100,
value=time,
onChangeEnd=set_time
),
),

Note the following design choices:

  • Each slider has a distinct color property to help users associate controls with parameters
  • The Badge components display the current value with matching colors for visual consistency
  • The onChangeEnd events trigger state updates whenever the user finishes adjusting a slider

Now we can move on to adding the plots to actually display the chart data we prepared above.

Line Plot Component

Recall that we want our first plot to show drawdown vs. distance along a line from the well using a line chart. For this, we will pivot away from Mantine and use a library more specialized for plotting: Plotly (see https://github.com/plotly/react-plotly.js/). This package is accessed on the pl namespace of lib and can be added as a nested component to the first GridCol(span=6) component like so:

lib.m.GridCol(span=6)(
lib.pl.Plot(
style=lib.Style(width="100%", height="100%"),
data=[
lib.Props(
x=distances.tolist(),
y=drawdown_line.tolist(),
type="scatter",
mode="lines+markers",
marker=lib.Props(color="red"),
),
],
layout=lib.Props(
autosize=True,
title=lib.Props(text="Theis Drawdown vs Distance"),
xaxis=lib.Props(
title=lib.Props(text="Distance from Well (m)"),
),
yaxis=lib.Props(
title=lib.Props(text="Drawdown (m)"),
),
),
)
),

Key components:

  • Plot: The Plotly component that renders interactive plots
  • style: Sets the plot dimensions to fill its container
  • data: A list of the datasets to be plotted, each with respective lists of x/y values and styling parameters. We only have one dataset to plot, and it's a "scatter" plot with red lines and markers.
  • layout: Configuration for titles, axes, and overall appearance

Heatmap Component

We want the second plot to show a 2D plan view of drawdown around the well using a heatmap. Again, we will use Plotly with slightly different properties and add it as a nested component to the second GridCol(span=6) component like so:, like so:

lib.m.GridCol(span=6)(
lib.pl.Plot(
style=lib.Style(width="100%", height="100%"),
data=[
lib.Props(
z=drawdown_2d.tolist(),
x=x.tolist(),
y=y.tolist(),
type="heatmap",
colorscale="Viridis",
reversescale=True,
),
],
layout=lib.Props(
autosize=True,
title=lib.Props(text="Plan View Drawdown Heat Map"),
xaxis=lib.Props(
title=lib.Props(text="Distance from Well (m)"),
),
yaxis=lib.Props(
title=lib.Props(text="Drawdown (m)"),
),
),
)
)

Key differences from the line plot:

  • type="heatmap": Creates a 2D colored grid instead of a line/scatter plot
  • z, x, y: For heatmaps, z represents the values to color, while x and y are coordinates
  • colorscale: The color scheme used (Viridis is a perceptually uniform colormap)
  • reversescale=True: Reverses the color direction so higher values are darker

Putting It All Together

We have now completely covered all of the code required to create your simple well drawdown calculator app, as contained within the home function. Just to be sure, your final home function should look like this:

@App.page
def home(lib):
# State management for reactive inputs
max_distance, set_max_distance = lib.hooks.use_state(100) # Max distance from well (m)
samples, set_samples = lib.hooks.use_state(100) # Number of distance samples for plotting
q, set_q = lib.hooks.use_state(1000) # Pumping rate (m3/day)
t, set_t = lib.hooks.use_state(100) # Transmissivity (m2/day)
s_exponent, set_s_exponent = lib.hooks.use_state(-3.0) # Storativity (unitless)
time, set_time = lib.hooks.use_state(10) # Time (days)

# Convert exponent to actual storativity value
s_actual = 10 ** s_exponent

# Create a 2D grid of x and y coordinates (e.g., -500m to 500m)
x = np.linspace(-max_distance, max_distance, samples)
y = np.linspace(-max_distance, max_distance, samples)
X, Y = np.meshgrid(x, y)

# Calculate radial distance 'r' for every point in the grid
R = np.sqrt(X**2 + Y**2)

# Model execution (triggers every time state changes)
distances = np.linspace(1, max_distance, samples)
drawdown_line = calculate_drawdown(q, t, s_actual, time, distances)
drawdown_2d = calculate_drawdown(q, t, s_actual, time, R)

# 4. Return the layout using Python components
return lib.tethys.Display(
lib.m.Grid(
lib.m.GridCol(span=12)(
lib.m.Title(order=1)("Well Drawdown Calculator"),
),
# Sidebar for inputs
lib.m.GridCol(span=12)(
lib.m.Group(
lib.m.Text(size="sm")("Max Distance from Well (m)"),
lib.m.Badge(color="yellow")(max_distance),
),
lib.m.Slider(
color="yellow",
value=max_distance,
min=50,
max=200,
step=5,
onChangeEnd=set_max_distance
),

lib.m.Group(
lib.m.Text(size="sm")("Samples"),
lib.m.Badge(color="turquoise")(samples),
),
lib.m.Slider(
color="turquoise",
value=samples,
min=10,
max=100,
step=5,
onChangeEnd=set_samples
),


lib.m.Group(
lib.m.Text(size="sm")("Pumping Rate (m³/day)"),
lib.m.Badge(color="blue")(q),
),
lib.m.Slider(
color="blue",
value=q,
min=100,
max=5000,
step=100,
onChangeEnd=set_q
),

lib.m.Group(
lib.m.Text(size="sm")("Transmissivity (m²/day)"),
lib.m.Badge(color="green")(t),
),
lib.m.Slider(
color="green",
min=10,
max=1000,
value=t,
onChangeEnd=set_t
),

lib.m.Group(
lib.m.Text(size="sm")("Storativity (log10(S))"),
lib.m.Badge(color="purple")(f"{s_exponent:.2f}"),
),
lib.m.Slider(
color="purple",
min=-5,
max=-1,
step=0.1,
value=s_exponent,
onChangeEnd=set_s_exponent
),

lib.m.Group(
lib.m.Text(size="sm")("Time (days)"),
lib.m.Badge(color="orange")(time),
),
lib.m.Slider(
color="orange",
min=1,
max=100,
value=time,
onChangeEnd=set_time
),
),

# Line plot of drawdown
lib.m.GridCol(span=6)(
lib.pl.Plot(
style=lib.Style(width="100%", height="100%"),
data=[
lib.Props(
x=distances.tolist(),
y=drawdown_line.tolist(),
type="scatter",
mode="lines+markers",
marker=lib.Props(color="red"),
),
],
layout=lib.Props(
autosize=True,
title=lib.Props(text="Theis Drawdown vs Distance"),
xaxis=lib.Props(
title=lib.Props(text="Distance from Well (m)"),
),
yaxis=lib.Props(
title=lib.Props(text="Drawdown (m)"),
),
),
)
),

# 2D contour plot of drawdown
lib.m.GridCol(span=6)(
lib.pl.Plot(
style=lib.Style(width="100%", height="100%"),
data=[
lib.Props(
z=drawdown_2d.tolist(),
x=x.tolist(),
y=y.tolist(),
type="heatmap",
colorscale="Viridis",
reversescale=True,
),
],
layout=lib.Props(
autosize=True,
title=lib.Props(text="Plan View Drawdown Heat Map"),
xaxis=lib.Props(
title=lib.Props(text="Distance from Well (m)"),
),
yaxis=lib.Props(
title=lib.Props(text="Drawdown (m)"),
),
),
)
)
)
)

Make sure you did not forget any commas. That is a common gotcha. Almost every line of the return block should end in either a comma (,) or an open parenthesis (().

Now it is time to go use the app yourself to see how it all works.

Using the App

Make sure your server is still running on the command line (recall that was with the tethys start command), and return back to your browser to view your app at http://localhost:8000/apps/well-drawdown-calculator.

You should now have a fully-functional app! Go ahead and click on various spots across the sliders and watch the graphs almost immediately update.

Now, to understand a bit about the underlying mechanisms at play.

Understanding the App Lifecycle

The State-Driven Workflow

In Tethys Component Apps, the entire page re-renders whenever state changes (i.e. whenever state variables are updated). This creates a predictable flow:

  1. User interacts with a component (moves slider, clicks button)
  2. Event handler calls a state setter function with the new value
  3. State is updated
  4. Page function re-executes from the top
  5. All calculations run with the new state values
  6. New component tree is generated
  7. UI updates to reflect the changes

Example: Adjusting Max Distance

Let's trace what happens when a user moves the "Max Distance from Well" slider:

User moves slider to 150

onChangeEnd=set_max_distance is called with 150

set_max_distance(150) updates the state

React triggers a re-render of the page function

max_distance is now 150 instead of 100

x = np.linspace(-150, 150, samples) creates new x-coordinates

R = np.sqrt(X**2 + Y**2) creates new distances in the 2D grid

drawdown_line = calculate_drawdown(..., distances) runs with new distances

drawdown_2d = calculate_drawdown(..., R) runs with new distances

Both Plot components receive new data

UI updates: plots show drawdown for the larger area

Performance Considerations

Since the page function re-executes on every state change, it's important to keep calculations efficient:

  • Numpy operations are fast and suitable for the scientific computations in our app
  • Simple transformations like creating arrays and calculating values happen instantly
  • Avoid expensive operations inside page functions without memoization

In our app, even with a 100×100 grid (10,000 points) and the complex Theis calculation, updates are nearly instantaneous on modern hardware. If that were not the case, and our model took longer to run, we'd have to explore a different paradigm in which the model runs in the background and the user is provided feedback via loading animations or something similar.


Summary

Congratulations! You now understand the core concepts of Tethys Component App development:

  • Scaffolding: Creating new apps with the tethys CLI
  • Page functions: Building interactive pages with the @App.page decorator
  • State management: Using lib.hooks.use_state for reactive variables
  • Components: Combining UI building blocks (Mantine, Plotly, Tethys) into complex interfaces
  • Event handling: Responding to user interactions with callbacks
  • Model integration: Exposing Python calculations through an interactive web interface
  • UX design: Creating effective, accessible interfaces for scientific applications

The well drawdown calculator serves as a template that you can adapt for other scientific models. The patterns you learned here—state management, component composition, event handling—apply to any Tethys Component App you develop in the future.

View the solution code for the Well Drawdown Calculator application here: https://github.com/tethysplatform/tethysapp-well_drawdown_calculator.

As you continue building Tethys apps, remember these key principles:

  1. Keep models separate: Business logic (like calculate_drawdown) should be independent of UI code
  2. Use state wisely: Every user-adjustable value should have a state variable
  3. Leverage third-party libraries: Mantine, Plotly, and other libraries provide powerful components
  4. Test interactively: The automatic reload feature makes development fast and feedback immediate
  5. Design for learning: Science apps should make it easy for users to understand relationships and patterns

Happy developing!