Formatters

Meerkat GUIs can display data of many different types, from images, to long-form text, to audio. Formatters control how these data types are displayed and interacted with in Meerkat GUIs.

For example, images can be displayed using the meerkat.format.ImageFormatter. Each formatter, specifies optional parameters that can be used to configure how the data is displayed. For example, the meerkat.format.ImageFormatter has a max_size parameter that can be used to specify the maximum size of the image to display.

import meerkat as mk
formatter = mk.format.ImageFormatter(max_size=(224, 224))

Formatter Group

Data in a Meerkat column sometimes need to be displayed differently in different GUI contexts. For example, in a table, we display thumbnails of images, but in a carousel view, we display the full image.

Because most components in Meerkat work on any data type, it is important that they are implemented in a formatter-agnostic way. So, instead of specifying formatters, components make requests for data specifying a formatter placeholder. For example, the mk.gui.Gallery component requests data using the thumbnail formatter placeholder.

For a specific column of data, we specify which formatters to use for each placeholder using a formatter group. A formatter group is a mapping from formatter placeholders to formatters. Each column in Meerkat has a formatter_group property. A column’s formatter group controls how it will be displayed in different contexts in Meerkat GUIs.

Much of the time, you don’t need to worry about specifying a formatter group, each column automatically populates its formatter group with sensible defaults.

For example, let’s take a look at the formatter group for a column of images in the imagenette dataset.

import meerkat as mk

df = mk.get("imagenette")
df["img"].formatter_group
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[2], line 4
      1 import meerkat as mk
      3 df = mk.get("imagenette")
----> 4 df["img"].formatter_group

File ~/work/meerkat/meerkat/meerkat/mixins/reactifiable.py:60, in ReactifiableMixin.__getattribute__(self, name)
     58 with unmarked():
     59     is_self_marked = super().__getattribute__("_self_marked")
---> 60     attr = super().__getattribute__(name)
     61     is_method_or_fn = inspect.ismethod(attr) or inspect.isfunction(attr)
     62     attr_is_reactive_fn = is_method_or_fn and is_reactive_fn(attr)

AttributeError: 'FileColumn' object has no attribute 'formatter_group'

This tells Meerkat components to use ImageFormatter(max_size=(224, 224)) when displaying thumbnails of column values, as in the mk.gui.Table component.

Changing Formatters

There are two ways you can use formatters to control how data is displayed in Meerkat GUIs.

  1. By updating a column’s formatter:

df["img"].formatter_group["thumbnail"] = ImageFormatter(max_size=(48, 48))
  1. By specifying it when creating a component:

df.columns # ['img', 'text']
gallery = Gallery(
  df=df.format(
    img={"thumbnail": ImageFormatter(max_size=(48, 48))},
    text={"icon": TextFormatter()},
  )
)

Implementing a Formatter

You can implement your own formatter for a custom data type. A formatter implementation must specify three things: a component_class, an encode method, a props method, a _get_state method, and a _set_state method.

Consider the following example of a formatter that encodes images as base64 strings and sends them to the frontend to be displayed using the Image component.

class ImageFormatter(Formatter):
    component_class = Image

    def __init__(
        self, max_size: Tuple[int] = None, classes: str = "", grayscale: str = False
    ):
        self.max_size = max_size
        self.classes = classes
        self.grayscale = grayscale

    def encode(self, cell: Image) -> str:
        with BytesIO() as buffer:
            if self.max_size:
                cell.thumbnail(self.max_size)
            cell.save(buffer, "jpeg")
            return "data:image/jpeg;base64,{im_base_64}".format(
                im_base_64=base64.b64encode(buffer.getvalue()).decode()
            )

    @property
    def props(self) -> Dict[str, Any]:
        return {"classes": self.classes, "grayscale": self.grayscale}

Let’s break down what’s happening here.

  • component_class specifies the class of the frontend component that should be created to display the data. In this case, it is the Image component defined below.

class Image(Component):
    data: str
    classes: str = ""
    grayscale: str = False
  • props specifies the values of the properties passed when constructing the components. Notice that the keys in the returned dictionary match the names of the properties defined in the Image component above.

  • encode specifies how a single cell from a column should be encoded on the Python side before being sent up to the frontend. In this case we are encoding an image as a base-64 string.

It’s also important to implement

Formatter Placeholders

As we discussed above, components specify formatter placeholders when requesting data from a column. This allows them to be formatter-agnostic, while still being able to display data in different ways depending on the context.

Formatter placeholders have special names, such as icon, focus, and thumbnail, which allow you to configure them when using Meerkat components in Python.

Components can use one or more of these placeholders (or define custom formatter placeholders) in order to change the encoding of the data fetched from the Python backend. They can pass formatter placeholders to the data fetching API provided by Meerkat, and data in the requested format is then delivered to them.

You can also define custom formatter placeholders that you want to associate with a frontend component that you might be building. For example, say you want to define a anonymous formatter that will be used to display data in an anonymized way e.g. to blur images, videos and text, and scramble audio.

class Anonymous(FormatterPlaceholder):
  """
  A placeholder that represents formatters that anonymize data.
  
  Ideally, should be configured by formatters that blur or 
  deidentify data e.g. blur images, videos, etc.
  """
  fallbacks: [Small]

Here, you use fallbacks to specify a formatter variable to use instead of anonymous if the user forgets to configure it. All formatter placeholders fallback to using base automatically, which uses a standard encoding of all data types to make sure everything can be displayed.

You would then use this anonymous variable in your frontend code when fetching data using the Meerkat js API.

// Fetch data from the backend using the anonymous formatter.
data = await fetchChunk({
  ...,
  formatter: "anonymous"
});

And you can implement custom formatters to achieve the effect of anonymizing data before sending it to the frontend e.g. using a blur filter over image data in the encode method of an ImageBlurFormatter.

When and where do we tell Meerkat how to associate formatter placeholders to these formatters? This is where formatter groups are used, which we discuss next.

As a user, when using a component with one or more formatter placeholders, you can pass in the formatter you want to use for each formatter variable. Meerkat will automatically ensure that the frontend gets data using the encode method associated with the formatter that was passed in.