{
"cells": [
{
"cell_type": "markdown",
"id": "4d9f1ab1",
"metadata": {},
"source": [
"# Tutorial 4: Reactive Functions (Reactive Image Viewer)\n",
"\n",
"In this tutorial, we will build a simple image viewer that shows a random subset of images from\n",
"a class in an image dataset.\n",
"\n",
"Through this tutorial, you will learn about:\n",
"- the concept of **reactive functions** in Meerkat\n",
"- how **chaining reactive functions** together can be used to build complex applications\n",
"- a few more components that you can use in Meerkat\n",
"\n",
"To get started, run the tutorial demo script.\n",
"```{code-block} bash\n",
"mk demo tutorial-reactive-viewer\n",
"```\n",
"You should see the tutorial app when you open the link in your browser.\n",
"\n",
"Let's break down the code in the demo script.\n",
"\n",
"## Data loading\n",
"\n",
"The first few lines just load in the `imagenette` dataset, a small 10-class subset of ImageNet.\n",
"```{margin}\n",
"`df` is an `mk.DataFrame`, which behaves quite similarly to a `pandas.DataFrame`.\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "76b425a5",
"metadata": {},
"outputs": [],
"source": [
"import meerkat as mk\n",
"import rich\n",
"\n",
"df = mk.get(\"imagenette\", version=\"160px\")\n",
"IMAGE_COL = \"img\"\n",
"LABEL_COL = \"label\""
]
},
{
"cell_type": "markdown",
"id": "608530fb",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "9b49e9d1",
"metadata": {
"tags": [
"remove-input"
]
},
"outputs": [
{
"data": {
"text/html": [
"
\n",
" \n",
" \n",
" | \n",
" path | \n",
" noisy_labels_0 | \n",
" noisy_labels_1 | \n",
" noisy_labels_5 | \n",
" noisy_labels_25 | \n",
" noisy_labels_50 | \n",
" is_valid | \n",
" label_id | \n",
" label | \n",
" label_idx | \n",
" split | \n",
" img_path | \n",
" img_id | \n",
" index | \n",
" img | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" train/n02979186/n02979186_9036.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/n02979186_9036.JPEG | \n",
" n02979186_9036 | \n",
" 0 | \n",
"  | \n",
"
\n",
" \n",
" 1 | \n",
" train/n02979186/n02979186_11957.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n03000684 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/n02979186_11957.JPEG | \n",
" n02979186_11957 | \n",
" 1 | \n",
"  | \n",
"
\n",
" \n",
" 2 | \n",
" train/n02979186/n02979186_9715.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n03417042 | \n",
" n03000684 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/n02979186_9715.JPEG | \n",
" n02979186_9715 | \n",
" 2 | \n",
"  | \n",
"
\n",
" \n",
" 3 | \n",
" train/n02979186/n02979186_21736.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n03417042 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/n02979186_21736.JPEG | \n",
" n02979186_21736 | \n",
" 3 | \n",
"  | \n",
"
\n",
" \n",
" 4 | \n",
" train/n02979186/ILSVRC2012_val_00046953.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n03394916 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/ILSVRC2012_val_00046953.JPEG | \n",
" ILSVRC2012_val_00046953 | \n",
" 4 | \n",
"  | \n",
"
\n",
" \n",
"
"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"df.head()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "e007e36c",
"metadata": {},
"outputs": [],
"source": [
"# Unique classes in the dataset.\n",
"labels = list(df[LABEL_COL].unique())"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "5130e646",
"metadata": {
"tags": [
"remove-input"
]
},
"outputs": [
{
"data": {
"text/html": [
"[\n",
" 'cassette player',\n",
" 'garbage truck',\n",
" 'tench',\n",
" 'english springer spaniel',\n",
" 'church',\n",
" 'parachute',\n",
" 'french horn',\n",
" 'chainsaw',\n",
" 'golf ball',\n",
" 'gas pump'\n",
"]\n",
"
\n"
],
"text/plain": [
"\u001b[1m[\u001b[0m\n",
" \u001b[32m'cassette player'\u001b[0m,\n",
" \u001b[32m'garbage truck'\u001b[0m,\n",
" \u001b[32m'tench'\u001b[0m,\n",
" \u001b[32m'english springer spaniel'\u001b[0m,\n",
" \u001b[32m'church'\u001b[0m,\n",
" \u001b[32m'parachute'\u001b[0m,\n",
" \u001b[32m'french horn'\u001b[0m,\n",
" \u001b[32m'chainsaw'\u001b[0m,\n",
" \u001b[32m'golf ball'\u001b[0m,\n",
" \u001b[32m'gas pump'\u001b[0m\n",
"\u001b[1m]\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"rich.print(labels)"
]
},
{
"cell_type": "markdown",
"id": "0ccbc783",
"metadata": {},
"source": [
"## Selecting a class\n",
"\n",
"Next, let's create a {class}`~meerkat.gui.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.\n",
"\n",
"```{margin}\n",
"A list of the components in Meerkat can be found in the\n",
"[component guide](../../guide/components/inbuilts.rst).\n",
"```\n",
"\n",
"Meerkat provides many components like `Select` that can be used to build useful\n",
"apps."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "2eeae05e",
"metadata": {},
"outputs": [],
"source": [
"# Give the user a way to select a class.\n",
"class_selector = mk.gui.Select(\n",
" values=list(labels),\n",
" value=labels[0],\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "9178e47c",
"metadata": {
"tags": [
"remove-input"
]
},
"outputs": [
{
"data": {
"text/html": [
"class_selector.value: \n",
" cassette player \n",
"type(class_selector.value): \n",
" <class 'meerkat.interactive.graph.store.Store'>\n",
"
\n"
],
"text/plain": [
"\u001b[34mclass_selector.value\u001b[0m: \n",
" cassette player \n",
"\u001b[1;34mtype\u001b[0m\u001b[1;34m(\u001b[0m\u001b[34mclass_selector.value\u001b[0m\u001b[1;34m)\u001b[0m: \n",
" \u001b[1m<\u001b[0m\u001b[1;95mclass\u001b[0m\u001b[39m \u001b[0m\u001b[32m'meerkat.interactive.graph.store.Store'\u001b[0m\u001b[1m>\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"rich.print(\n",
" \"[blue]class_selector.value[/blue]:\", \n",
" f\"\\n\\t{str(class_selector.value)}\",\n",
" \"\\n[blue]type(class_selector.value)[/blue]:\", \n",
" f\"\\n\\t{str(type(class_selector.value))}\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "18db2873",
"metadata": {},
"source": [
"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.\n",
"\n",
"\n",
"You can access the value of a `Store` object by using the `.value` attribute e.g."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "843c7beb",
"metadata": {
"tags": [
"remove-input"
]
},
"outputs": [
{
"data": {
"text/html": [
"class_selector.value.value: \n",
" cassette player \n",
"type(class_selector.value.value): \n",
" <class 'str'>\n",
"
\n"
],
"text/plain": [
"\u001b[34mclass_selector.value.value\u001b[0m: \n",
" cassette player \n",
"\u001b[1;34mtype\u001b[0m\u001b[1;34m(\u001b[0m\u001b[34mclass_selector.value.value\u001b[0m\u001b[1;34m)\u001b[0m: \n",
" \u001b[1m<\u001b[0m\u001b[1;95mclass\u001b[0m\u001b[39m \u001b[0m\u001b[32m'str'\u001b[0m\u001b[1m>\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"rich.print(\n",
" \"[blue]class_selector.value.value[/blue]:\", \n",
" f\"\\n\\t{str(class_selector.value.value)}\",\n",
" \"\\n[blue]type(class_selector.value.value)[/blue]:\", \n",
" f\"\\n\\t{str(type(class_selector.value.value))}\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "1cbc633b",
"metadata": {},
"source": [
"We can also look at the `class_selector` component itself."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "017eb1a7",
"metadata": {
"tags": [
"remove-input"
]
},
"outputs": [
{
"data": {
"text/html": [
"Select(\n",
" values=Store(['cassette player', 'garbage truck', 'tench', 'english springer spaniel', 'church', 'parachute', \n",
"'french horn', 'chainsaw', 'golf ball', 'gas pump']),\n",
" labels=Store(['cassette player', 'garbage truck', 'tench', 'english springer spaniel', 'church', 'parachute', \n",
"'french horn', 'chainsaw', 'golf ball', 'gas pump']),\n",
" value=Store('cassette player'),\n",
" disabled=Store(False),\n",
" classes=Store(''),\n",
" on_change=None,\n",
" _slots=[],\n",
" _self_id='__mkid__5ec9beda37e944068b55e1d67904c251'\n",
")\n",
"
\n"
],
"text/plain": [
"\u001b[1;35mSelect\u001b[0m\u001b[1m(\u001b[0m\n",
" \u001b[33mvalues\u001b[0m=\u001b[1;35mStore\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[32m'cassette player'\u001b[0m, \u001b[32m'garbage truck'\u001b[0m, \u001b[32m'tench'\u001b[0m, \u001b[32m'english springer spaniel'\u001b[0m, \u001b[32m'church'\u001b[0m, \u001b[32m'parachute'\u001b[0m, \n",
"\u001b[32m'french horn'\u001b[0m, \u001b[32m'chainsaw'\u001b[0m, \u001b[32m'golf ball'\u001b[0m, \u001b[32m'gas pump'\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n",
" \u001b[33mlabels\u001b[0m=\u001b[1;35mStore\u001b[0m\u001b[1m(\u001b[0m\u001b[1m[\u001b[0m\u001b[32m'cassette player'\u001b[0m, \u001b[32m'garbage truck'\u001b[0m, \u001b[32m'tench'\u001b[0m, \u001b[32m'english springer spaniel'\u001b[0m, \u001b[32m'church'\u001b[0m, \u001b[32m'parachute'\u001b[0m, \n",
"\u001b[32m'french horn'\u001b[0m, \u001b[32m'chainsaw'\u001b[0m, \u001b[32m'golf ball'\u001b[0m, \u001b[32m'gas pump'\u001b[0m\u001b[1m]\u001b[0m\u001b[1m)\u001b[0m,\n",
" \u001b[33mvalue\u001b[0m=\u001b[1;35mStore\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'cassette player'\u001b[0m\u001b[1m)\u001b[0m,\n",
" \u001b[33mdisabled\u001b[0m=\u001b[1;35mStore\u001b[0m\u001b[1m(\u001b[0m\u001b[3;91mFalse\u001b[0m\u001b[1m)\u001b[0m,\n",
" \u001b[33mclasses\u001b[0m=\u001b[1;35mStore\u001b[0m\u001b[1m(\u001b[0m\u001b[32m''\u001b[0m\u001b[1m)\u001b[0m,\n",
" \u001b[33mon_change\u001b[0m=\u001b[3;35mNone\u001b[0m,\n",
" \u001b[33m_slots\u001b[0m=\u001b[1m[\u001b[0m\u001b[1m]\u001b[0m,\n",
" \u001b[33m_self_id\u001b[0m=\u001b[32m'__mkid__5ec9beda37e944068b55e1d67904c251'\u001b[0m\n",
"\u001b[1m)\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"rich.print(class_selector)"
]
},
{
"cell_type": "markdown",
"id": "7460e62c",
"metadata": {},
"source": [
"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.\n",
"\n",
"## Filtering the dataset by class\n",
"\n",
"Once the user selects a class, the dataset should be filtered to that class. \n",
"**Importantly, we want this to happen every time the user selects a new class.**\n",
"\n",
"Let's think about what would happen if we did the obvious thing and just filtered the dataset once."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "fec5bd95",
"metadata": {},
"outputs": [],
"source": [
"# Filter the dataset to the selected class. Do it normally.\n",
"filtered_df = df[df[LABEL_COL] == class_selector.value]"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "c2324698",
"metadata": {
"tags": [
"remove-input"
]
},
"outputs": [
{
"data": {
"text/html": [
"filtered_df: \n",
" DataFrame(nrows: 1350, ncols: 15)\n",
"
\n"
],
"text/plain": [
"\u001b[34mfiltered_df\u001b[0m: \n",
" \u001b[1;35mDataFrame\u001b[0m\u001b[1m(\u001b[0mnrows: \u001b[1;36m1350\u001b[0m, ncols: \u001b[1;36m15\u001b[0m\u001b[1m)\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"rich.print(\n",
" \"[blue]filtered_df[/blue]:\", \n",
" f\"\\n\\t{str(filtered_df)}\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "0843b893",
"metadata": {},
"source": [
"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.**\n",
"\n",
"\n",
"This is where **reactive functions** come in. A reactive function is a function that is automatically re-executed whenever one of its inputs updates. \n",
"\n",
"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 {py:func}`@reactive() ` decorator."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "67d47567",
"metadata": {},
"outputs": [],
"source": [
"# Filter the dataset to the selected class. Use a reactive function.\n",
"@mk.reactive()\n",
"def filter_by_class(df: mk.DataFrame, label: str):\n",
" return df[df[LABEL_COL] == label]"
]
},
{
"cell_type": "markdown",
"id": "74694071",
"metadata": {},
"source": [
"Note a couple of things here:\n",
"- The function `filter_by_class` is decorated with `@mk.reactive()`. This is what makes it reactive.\n",
"- `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.\n",
"\n",
"Let's now call this reactive function on the dataset and the `Store` object `class_selector.value`."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "d6918e60",
"metadata": {},
"outputs": [],
"source": [
"filtered_df = filter_by_class(df, class_selector.value)"
]
},
{
"cell_type": "markdown",
"id": "40bb1876",
"metadata": {},
"source": [
"```{margin}\n",
"In Meerkat, only `marked` objects can cause a reactive function to re-execute when they are updated. All Meerkat objects have `.mark()` and `.unmark()` methods, and only `Store` objects are marked by default. You can read more about how this works in the [user guide on reactive functions](../../guide/reactive-functions/concepts.md).\n",
"```\n",
"\n",
"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!** \n",
"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`.\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "97068f93",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
" \n",
" \n",
" | \n",
" path | \n",
" noisy_labels_0 | \n",
" noisy_labels_1 | \n",
" noisy_labels_5 | \n",
" noisy_labels_25 | \n",
" noisy_labels_50 | \n",
" is_valid | \n",
" label_id | \n",
" label | \n",
" label_idx | \n",
" split | \n",
" img_path | \n",
" img_id | \n",
" index | \n",
" img | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" train/n02979186/n02979186_9036.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/n02979186_9036.JPEG | \n",
" n02979186_9036 | \n",
" 0 | \n",
"  | \n",
"
\n",
" \n",
" 1 | \n",
" train/n02979186/n02979186_11957.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n03000684 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/n02979186_11957.JPEG | \n",
" n02979186_11957 | \n",
" 1 | \n",
"  | \n",
"
\n",
" \n",
" 2 | \n",
" train/n02979186/n02979186_9715.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n03417042 | \n",
" n03000684 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/n02979186_9715.JPEG | \n",
" n02979186_9715 | \n",
" 2 | \n",
"  | \n",
"
\n",
" \n",
" 3 | \n",
" train/n02979186/n02979186_21736.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n03417042 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/n02979186_21736.JPEG | \n",
" n02979186_21736 | \n",
" 3 | \n",
"  | \n",
"
\n",
" \n",
" 4 | \n",
" train/n02979186/ILSVRC2012_val_00046953.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n03394916 | \n",
" False | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" train | \n",
" train/n02979186/ILSVRC2012_val_00046953.JPEG | \n",
" ILSVRC2012_val_00046953 | \n",
" 4 | \n",
"  | \n",
"
\n",
" \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
"
\n",
" \n",
" 1345 | \n",
" val/n02979186/n02979186_11481.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" True | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" valid | \n",
" val/n02979186/n02979186_11481.JPEG | \n",
" n02979186_11481 | \n",
" 9821 | \n",
"  | \n",
"
\n",
" \n",
" 1346 | \n",
" val/n02979186/n02979186_27481.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" True | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" valid | \n",
" val/n02979186/n02979186_27481.JPEG | \n",
" n02979186_27481 | \n",
" 9822 | \n",
"  | \n",
"
\n",
" \n",
" 1347 | \n",
" val/n02979186/n02979186_11.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" True | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" valid | \n",
" val/n02979186/n02979186_11.JPEG | \n",
" n02979186_11 | \n",
" 9823 | \n",
"  | \n",
"
\n",
" \n",
" 1348 | \n",
" val/n02979186/n02979186_26822.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" True | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" valid | \n",
" val/n02979186/n02979186_26822.JPEG | \n",
" n02979186_26822 | \n",
" 9824 | \n",
"  | \n",
"
\n",
" \n",
" 1349 | \n",
" val/n02979186/n02979186_12681.JPEG | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" n02979186 | \n",
" True | \n",
" n02979186 | \n",
" cassette player | \n",
" 482 | \n",
" valid | \n",
" val/n02979186/n02979186_12681.JPEG | \n",
" n02979186_12681 | \n",
" 9825 | \n",
"  | \n",
"
\n",
" \n",
"
"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"filter_by_class(df, \"cassette player\")"
]
},
{
"cell_type": "markdown",
"id": "625eddad",
"metadata": {},
"source": [
"Let's also note a couple of other ways in which we could create reactive functions that wouldn't quite have worked."
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "13e2ba50",
"metadata": {},
"outputs": [],
"source": [
"@mk.reactive()\n",
"def filter_by_class(df: mk.DataFrame):\n",
" return df[df[LABEL_COL] == class_selector.value]"
]
},
{
"cell_type": "markdown",
"id": "eb148ae3",
"metadata": {},
"source": [
"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!\n",
"\n",
"So far so good. We've created and used a reactive function that filters the dataset to the selected class. \n",
"\n",
"## Selecting a random subset of images\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "c95acaf8",
"metadata": {},
"outputs": [],
"source": [
"\"\"\"Select a random subset of images from the filtered dataset.\"\"\"\n",
"@mk.reactive()\n",
"def random_images(df: mk.DataFrame):\n",
" # Sample 16 images from the filtered dataset.\n",
" # `images` will be a `Column` object.\n",
" images = df.sample(16)[IMAGE_COL]\n",
"\n",
" # Encode the images as base64 strings.\n",
" # Use a `Formatter` object to do this.\n",
" formatter = images.formatters['base']\n",
"\n",
" # All Formatter objects have an `encode` method that\n",
" # can be used to take a data object and encode it in some way.\n",
" return [formatter.encode(img) for img in images]"
]
},
{
"cell_type": "markdown",
"id": "87902344",
"metadata": {},
"source": [
"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.\n",
"\n",
"Let's pass the output of `filter_by_class` to `random_images`."
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "59cb817a",
"metadata": {},
"outputs": [],
"source": [
"images = random_images(filtered_df)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "440f5996",
"metadata": {
"tags": [
"remove-input"
]
},
"outputs": [
{
"data": {
"text/html": [
"images: \n",
" \n",
"
\n"
],
"text/plain": [
"\u001b[34mimages\u001b[0m: \n",
" \n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"rich.print(\n",
" \"[blue]images[/blue]:\", \n",
" f\"\\n\\t[{str(images[-1])}, ...]\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "b996c58e",
"metadata": {},
"source": [
"This sets up a chain of reactive functions that will be re-executed whenever the user selects a new class.\n",
"\n",
"## Displaying the images\n",
"Finally, let's show the user the images using a grid of `Image` components."
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "e7930563",
"metadata": {
"tags": [
"remove-output"
]
},
"outputs": [],
"source": [
"# Make a grid with 4 columns\n",
"grid = mk.gui.html.gridcols4([\n",
" # Use equal-sized square boxes in the grid\n",
" mk.gui.html.div(\n",
" # Wrap the image in a `mk.gui.Image` component\n",
" mk.gui.Image(data=img), \n",
" style=\"aspect-ratio: 1 / 1\",\n",
" )\n",
" for img in images\n",
"], classes=\"gap-2\") # Add some spacing in the grid."
]
},
{
"cell_type": "markdown",
"id": "4cd0cdac",
"metadata": {},
"source": [
"Many components in Meerkat accept \n",
"- a `classes` attribute that can be used to add Tailwind CSS classes to the component, and\n",
"- a `style` attribute that can be used to add inline CSS styles to the component.\n",
"\n",
"Finally, let's use the `flexcol` component to stack the class selector and the grid of images vertically."
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "1b08b8b8",
"metadata": {},
"outputs": [],
"source": [
"layout = mk.gui.html.flexcol([\n",
" mk.gui.html.div(\n",
" [mk.gui.Caption(\"Choose a class:\"), class_selector], \n",
" classes=\"flex justify-center items-center mb-2 gap-4\"\n",
" ),\n",
" grid,\n",
"])"
]
},
{
"cell_type": "markdown",
"id": "ae4e5962",
"metadata": {},
"source": [
"We can pass everything into a `Page` component to render the app."
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "7655f7ce",
"metadata": {
"tags": [
"remove-output"
]
},
"outputs": [
{
"data": {
"text/html": [
"Frontend is not initialized. Running `mk.gui.start()`.\n",
"
\n"
],
"text/plain": [
"Frontend is not initialized. Running `\u001b[1;35mmk.gui.start\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m`.\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "db986feec35d428797db64ad162ab976",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Downloading: 0%| | 0.00/2.83M [00:00, ?B/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
" \n",
" "
],
"text/plain": [
""
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"page = mk.gui.Page(component=layout, id=\"tutorial-2\")\n",
"page.launch()"
]
},
{
"cell_type": "markdown",
"id": "bb022eab",
"metadata": {},
"source": [
"That's it!"
]
}
],
"metadata": {
"file_format": "mystnb",
"kernelspec": {
"display_name": "python3",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.17"
},
"source_map": [
5,
31,
38,
41,
46,
51,
54,
69,
77,
85,
93,
101,
106,
109,
120,
125,
131,
140,
145,
153,
155,
166,
168,
172,
176,
185,
200,
206,
210,
216,
223,
235,
242,
250,
254,
258
],
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {
"41413972604c434c98e41a610f9a6378": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HTMLStyleModel",
"state": {
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HTMLStyleModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "StyleView",
"background": null,
"description_width": "",
"font_size": null,
"text_color": null
}
},
"4ddb0bb486414c3aa72d03979fae1da9": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"631ae16b8cf54519beb08917c75b135e": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HTMLStyleModel",
"state": {
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HTMLStyleModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "StyleView",
"background": null,
"description_width": "",
"font_size": null,
"text_color": null
}
},
"7ec519b0c25944c18395a7b3f8db7439": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"8498702ee5664a74bd3f25f86e2e07d4": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"8f81ff78f52a46a48a19c71a2852ed71": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"9f76d292c2dd4ba0b7f701b6ab55d0f6": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HTMLModel",
"state": {
"_dom_classes": [],
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HTMLModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/controls",
"_view_module_version": "2.0.0",
"_view_name": "HTMLView",
"description": "",
"description_allow_html": false,
"layout": "IPY_MODEL_8498702ee5664a74bd3f25f86e2e07d4",
"placeholder": "",
"style": "IPY_MODEL_41413972604c434c98e41a610f9a6378",
"tabbable": null,
"tooltip": null,
"value": "Downloading: 100%"
}
},
"ac98b1af7b324036ae16be316bed0be7": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "FloatProgressModel",
"state": {
"_dom_classes": [],
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "FloatProgressModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/controls",
"_view_module_version": "2.0.0",
"_view_name": "ProgressView",
"bar_style": "success",
"description": "",
"description_allow_html": false,
"layout": "IPY_MODEL_4ddb0bb486414c3aa72d03979fae1da9",
"max": 2827797.0,
"min": 0.0,
"orientation": "horizontal",
"style": "IPY_MODEL_d3c92494819740ad8dd6bc8dd753657e",
"tabbable": null,
"tooltip": null,
"value": 2827797.0
}
},
"d3c92494819740ad8dd6bc8dd753657e": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "ProgressStyleModel",
"state": {
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "ProgressStyleModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "StyleView",
"bar_color": null,
"description_width": ""
}
},
"d772f397a77545ea97c09d6d924e2fe1": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HTMLModel",
"state": {
"_dom_classes": [],
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HTMLModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/controls",
"_view_module_version": "2.0.0",
"_view_name": "HTMLView",
"description": "",
"description_allow_html": false,
"layout": "IPY_MODEL_8f81ff78f52a46a48a19c71a2852ed71",
"placeholder": "",
"style": "IPY_MODEL_631ae16b8cf54519beb08917c75b135e",
"tabbable": null,
"tooltip": null,
"value": " 2.83M/2.83M [00:00<00:00, 36.1MB/s]"
}
},
"db986feec35d428797db64ad162ab976": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HBoxModel",
"state": {
"_dom_classes": [],
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HBoxModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/controls",
"_view_module_version": "2.0.0",
"_view_name": "HBoxView",
"box_style": "",
"children": [
"IPY_MODEL_9f76d292c2dd4ba0b7f701b6ab55d0f6",
"IPY_MODEL_ac98b1af7b324036ae16be316bed0be7",
"IPY_MODEL_d772f397a77545ea97c09d6d924e2fe1"
],
"layout": "IPY_MODEL_7ec519b0c25944c18395a7b3f8db7439",
"tabbable": null,
"tooltip": null
}
}
},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 5
}