Tutorial 4: Reactive Functions (Reactive Image Viewer)¶

In this tutorial, we will build a simple image viewer that shows a random subset of images from a class in an image dataset.

Through this tutorial, you will learn about:

  • the concept of reactive functions in Meerkat

  • how chaining reactive functions together can be used to build complex applications

  • a few more components that you can use in Meerkat

To get started, run the tutorial demo script.

mk demo tutorial-reactive-viewer

You should see the tutorial app when you open the link in your browser.

Let’s break down the code in the demo script.

Data loading¶

The first few lines just load in the imagenette dataset, a small 10-class subset of ImageNet.

import meerkat as mk
import rich

df = mk.get("imagenette", version="160px")
IMAGE_COL = "img"
LABEL_COL = "label"
path noisy_labels_0 noisy_labels_1 noisy_labels_5 noisy_labels_25 noisy_labels_50 is_valid label_id label label_idx split img_path img_id index img
0 train/n02979186/n02979186_9036.JPEG n02979186 n02979186 n02979186 n02979186 n02979186 False n02979186 cassette player 482 train train/n02979186/n02979186_9036.JPEG n02979186_9036 0
1 train/n02979186/n02979186_11957.JPEG n02979186 n02979186 n02979186 n02979186 n03000684 False n02979186 cassette player 482 train train/n02979186/n02979186_11957.JPEG n02979186_11957 1
2 train/n02979186/n02979186_9715.JPEG n02979186 n02979186 n02979186 n03417042 n03000684 False n02979186 cassette player 482 train train/n02979186/n02979186_9715.JPEG n02979186_9715 2
3 train/n02979186/n02979186_21736.JPEG n02979186 n02979186 n02979186 n02979186 n03417042 False n02979186 cassette player 482 train train/n02979186/n02979186_21736.JPEG n02979186_21736 3
4 train/n02979186/ILSVRC2012_val_00046953.JPEG n02979186 n02979186 n02979186 n02979186 n03394916 False n02979186 cassette player 482 train train/n02979186/ILSVRC2012_val_00046953.JPEG ILSVRC2012_val_00046953 4
# Unique classes in the dataset.
labels = list(df[LABEL_COL].unique())
[
    'cassette player',
    'garbage truck',
    'tench',
    'english springer spaniel',
    'church',
    'parachute',
    'french horn',
    'chainsaw',
    'golf ball',
    'gas pump'
]

Selecting a class¶

Next, let’s create a Select component that allows the user to select a class. Once a user picks the class, we’ll then show a random subset of images from that class.

Meerkat provides many components like Select that can be used to build useful apps.

# Give the user a way to select a class.
class_selector = mk.gui.Select(
    values=list(labels),
    value=labels[0],
)
class_selector.value: 
        cassette player 
type(class_selector.value): 
        <class 'meerkat.interactive.graph.store.Store'>

Notice here that class_selector.value is a Store object and not a str! A Store is a special object in Meerkat that serves as a thin wrapper around any Python object. It’s incredibly useful to connect different components to each other, and we’ll see more about how it plays a role in the next section.

You can access the value of a Store object by using the .value attribute e.g.

class_selector.value.value: 
        cassette player 
type(class_selector.value.value): 
        <class 'str'>

We can also look at the class_selector component itself.

Select(
    values=Store(['cassette player', 'garbage truck', 'tench', 'english springer spaniel', 'church', 'parachute', 
'french horn', 'chainsaw', 'golf ball', 'gas pump']),
    labels=Store(['cassette player', 'garbage truck', 'tench', 'english springer spaniel', 'church', 'parachute', 
'french horn', 'chainsaw', 'golf ball', 'gas pump']),
    value=Store('cassette player'),
    disabled=Store(False),
    classes=Store(''),
    on_change=None,
    _slots=[],
    _self_id='__mkid__5ec9beda37e944068b55e1d67904c251'
)

Notice that all of the attributes of the Select component are also Store objects. This is done automatically by all classes that subclass Component in Meerkat, of which Select is one. This is very useful because as Store objects all of these attributes are synchronized between the frontend interface and the backend Python code.

Filtering the dataset by class¶

Once the user selects a class, the dataset should be filtered to that class. Importantly, we want this to happen every time the user selects a new class.

Let’s think about what would happen if we did the obvious thing and just filtered the dataset once.

# Filter the dataset to the selected class. Do it normally.
filtered_df = df[df[LABEL_COL] == class_selector.value]
filtered_df: 
        DataFrame(nrows: 1350, ncols: 15)

The problem that will arise is that this filter uses the class label in class_selector.value at the time of script execution. Once the user chooses a different class, there’s no way to go back to this line of code and rerun it.

This is where reactive functions come in. A reactive function is a function that is automatically re-executed whenever one of its inputs updates.

In this example, we would want to re-execute the filter whenever the user selects a class and class_selector.value changes. Let’s write a simple reactive function that filters the dataset to the selected class, using the @reactive() decorator.

# Filter the dataset to the selected class. Use a reactive function.
@mk.reactive()
def filter_by_class(df: mk.DataFrame, label: str):
    return df[df[LABEL_COL] == label]

Note a couple of things here:

  • The function filter_by_class is decorated with @mk.reactive(). This is what makes it reactive.

  • filter_by_class is written as a normal Python function. This is true in general for reactive functions: there’s no special syntax or anything.

Let’s now call this reactive function on the dataset and the Store object class_selector.value.

filtered_df = filter_by_class(df, class_selector.value)

When calling filter_by_class, we pass it the DataFrame and the Store object class_selector.value, and not the actual string value of the class! This is critical to understand: in order for an argument to trigger re-execution of a reactive function, it must be a Meerkat object like a Store or DataFrame, and not a Python object like str.

Of course, reactive functions can be used like normal Python functions, e.g. it would be perfectly fine to pass in str objects to filter_by_class instead of Store objects.

filter_by_class(df, "cassette player")
path noisy_labels_0 noisy_labels_1 noisy_labels_5 noisy_labels_25 noisy_labels_50 is_valid label_id label label_idx split img_path img_id index img
0 train/n02979186/n02979186_9036.JPEG n02979186 n02979186 n02979186 n02979186 n02979186 False n02979186 cassette player 482 train train/n02979186/n02979186_9036.JPEG n02979186_9036 0
1 train/n02979186/n02979186_11957.JPEG n02979186 n02979186 n02979186 n02979186 n03000684 False n02979186 cassette player 482 train train/n02979186/n02979186_11957.JPEG n02979186_11957 1
2 train/n02979186/n02979186_9715.JPEG n02979186 n02979186 n02979186 n03417042 n03000684 False n02979186 cassette player 482 train train/n02979186/n02979186_9715.JPEG n02979186_9715 2
3 train/n02979186/n02979186_21736.JPEG n02979186 n02979186 n02979186 n02979186 n03417042 False n02979186 cassette player 482 train train/n02979186/n02979186_21736.JPEG n02979186_21736 3
4 train/n02979186/ILSVRC2012_val_00046953.JPEG n02979186 n02979186 n02979186 n02979186 n03394916 False n02979186 cassette player 482 train train/n02979186/ILSVRC2012_val_00046953.JPEG ILSVRC2012_val_00046953 4
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1345 val/n02979186/n02979186_11481.JPEG n02979186 n02979186 n02979186 n02979186 n02979186 True n02979186 cassette player 482 valid val/n02979186/n02979186_11481.JPEG n02979186_11481 9821
1346 val/n02979186/n02979186_27481.JPEG n02979186 n02979186 n02979186 n02979186 n02979186 True n02979186 cassette player 482 valid val/n02979186/n02979186_27481.JPEG n02979186_27481 9822
1347 val/n02979186/n02979186_11.JPEG n02979186 n02979186 n02979186 n02979186 n02979186 True n02979186 cassette player 482 valid val/n02979186/n02979186_11.JPEG n02979186_11 9823
1348 val/n02979186/n02979186_26822.JPEG n02979186 n02979186 n02979186 n02979186 n02979186 True n02979186 cassette player 482 valid val/n02979186/n02979186_26822.JPEG n02979186_26822 9824
1349 val/n02979186/n02979186_12681.JPEG n02979186 n02979186 n02979186 n02979186 n02979186 True n02979186 cassette player 482 valid val/n02979186/n02979186_12681.JPEG n02979186_12681 9825

Let’s also note a couple of other ways in which we could create reactive functions that wouldn’t quite have worked.

@mk.reactive()
def filter_by_class(df: mk.DataFrame):
    return df[df[LABEL_COL] == class_selector.value]

Here, we’ve forgotten to create an argument for the class label, so using this reactive function would not work. It would never re-run when the user selects a new class!

So far so good. We’ve created and used a reactive function that filters the dataset to the selected class.

Selecting a random subset of images¶

Let’s now create another reactive function that selects a random subset of images from the filtered dataset. Then, we’ll chain together the two reactive functions we’ve created so far to get the final result.

"""Select a random subset of images from the filtered dataset."""
@mk.reactive()
def random_images(df: mk.DataFrame):
    # Sample 16 images from the filtered dataset.
    # `images` will be a `Column` object.
    images = df.sample(16)[IMAGE_COL]

    # Encode the images as base64 strings.
    # Use a `Formatter` object to do this.
    formatter = images.formatters['base']

    # All Formatter objects have an `encode` method that
    # can be used to take a data object and encode it in some way.
    return [formatter.encode(img) for img in images]

Here, random_images takes in a DataFrame and returns a list of base64-encoded images. It’s decorated with @mk.reactive() so that it will be re-executed whenever the DataFrame is updated.

Let’s pass the output of filter_by_class to random_images.

images = random_images(filtered_df)
images: 
        

This sets up a chain of reactive functions that will be re-executed whenever the user selects a new class.

Displaying the images¶

Finally, let’s show the user the images using a grid of Image components.

# Make a grid with 4 columns
grid = mk.gui.html.gridcols4([
    # Use equal-sized square boxes in the grid
    mk.gui.html.div(
        # Wrap the image in a `mk.gui.Image` component
        mk.gui.Image(data=img), 
        style="aspect-ratio: 1 / 1",
    )
    for img in images
], classes="gap-2") # Add some spacing in the grid.

Many components in Meerkat accept

  • a classes attribute that can be used to add Tailwind CSS classes to the component, and

  • a style attribute that can be used to add inline CSS styles to the component.

Finally, let’s use the flexcol component to stack the class selector and the grid of images vertically.

layout = mk.gui.html.flexcol([
    mk.gui.html.div(
        [mk.gui.Caption("Choose a class:"), class_selector], 
        classes="flex justify-center items-center mb-2 gap-4"
    ),
    grid,
])

We can pass everything into a Page component to render the app.

page = mk.gui.Page(component=layout, id="tutorial-2")
page.launch()

That’s it!