{ "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
pathnoisy_labels_0noisy_labels_1noisy_labels_5noisy_labels_25noisy_labels_50is_validlabel_idlabellabel_idxsplitimg_pathimg_idindeximg
0train/n02979186/n02979186_9036.JPEGn02979186n02979186n02979186n02979186n02979186Falsen02979186cassette player482traintrain/n02979186/n02979186_9036.JPEGn02979186_90360
1train/n02979186/n02979186_11957.JPEGn02979186n02979186n02979186n02979186n03000684Falsen02979186cassette player482traintrain/n02979186/n02979186_11957.JPEGn02979186_119571
2train/n02979186/n02979186_9715.JPEGn02979186n02979186n02979186n03417042n03000684Falsen02979186cassette player482traintrain/n02979186/n02979186_9715.JPEGn02979186_97152
3train/n02979186/n02979186_21736.JPEGn02979186n02979186n02979186n02979186n03417042Falsen02979186cassette player482traintrain/n02979186/n02979186_21736.JPEGn02979186_217363
4train/n02979186/ILSVRC2012_val_00046953.JPEGn02979186n02979186n02979186n02979186n03394916Falsen02979186cassette player482traintrain/n02979186/ILSVRC2012_val_00046953.JPEGILSVRC2012_val_000469534
" ], "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
pathnoisy_labels_0noisy_labels_1noisy_labels_5noisy_labels_25noisy_labels_50is_validlabel_idlabellabel_idxsplitimg_pathimg_idindeximg
0train/n02979186/n02979186_9036.JPEGn02979186n02979186n02979186n02979186n02979186Falsen02979186cassette player482traintrain/n02979186/n02979186_9036.JPEGn02979186_90360
1train/n02979186/n02979186_11957.JPEGn02979186n02979186n02979186n02979186n03000684Falsen02979186cassette player482traintrain/n02979186/n02979186_11957.JPEGn02979186_119571
2train/n02979186/n02979186_9715.JPEGn02979186n02979186n02979186n03417042n03000684Falsen02979186cassette player482traintrain/n02979186/n02979186_9715.JPEGn02979186_97152
3train/n02979186/n02979186_21736.JPEGn02979186n02979186n02979186n02979186n03417042Falsen02979186cassette player482traintrain/n02979186/n02979186_21736.JPEGn02979186_217363
4train/n02979186/ILSVRC2012_val_00046953.JPEGn02979186n02979186n02979186n02979186n03394916Falsen02979186cassette player482traintrain/n02979186/ILSVRC2012_val_00046953.JPEGILSVRC2012_val_000469534
................................................
1345val/n02979186/n02979186_11481.JPEGn02979186n02979186n02979186n02979186n02979186Truen02979186cassette player482validval/n02979186/n02979186_11481.JPEGn02979186_114819821
1346val/n02979186/n02979186_27481.JPEGn02979186n02979186n02979186n02979186n02979186Truen02979186cassette player482validval/n02979186/n02979186_27481.JPEGn02979186_274819822
1347val/n02979186/n02979186_11.JPEGn02979186n02979186n02979186n02979186n02979186Truen02979186cassette player482validval/n02979186/n02979186_11.JPEGn02979186_119823
1348val/n02979186/n02979186_26822.JPEGn02979186n02979186n02979186n02979186n02979186Truen02979186cassette player482validval/n02979186/n02979186_26822.JPEGn02979186_268229824
1349val/n02979186/n02979186_12681.JPEGn02979186n02979186n02979186n02979186n02979186Truen02979186cassette player482validval/n02979186/n02979186_12681.JPEGn02979186_126819825
" ], "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\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 }