Skip to content

Design your view

Annotators review faster with an interface fitted to their task: the text large in the middle, the fields about the whole note next to it, and keyboard shortcuts for the labels they use most. In this tutorial, you turn the review_entities view into such an interface, and see what else DataWidgetFactory widgets can show.

Before you start

Follow Review entities first: this tutorial builds on its oncology.pret file and its view.

Add fields about each note

Besides its entities, annotators should tell for each note the site of the cancer, and mark it as reviewed. Add these two columns to the notes of the file:

from pret import DataFrame, load_store

store = load_store("~/metanno-home/annotations/oncology.pret")
notes = store["notes"].materialize().to_pandas()
store["notes"] = DataFrame(notes.assign(reviewed=False, site=None).astype({"site": "string"}))

site starts empty, and its string type tells Metanno which values it will hold.

Write the interface

The new view arranges four panels: the notes on the left, the text in the middle, and on the right a form with the note questions above the entities of the note:

%%writefile ~/metanno-home/recipes/oncology_review.py
from typing import Annotated, TypedDict

from pret import DataFrame
from pret_joy import Divider, Stack
from pret_simple_dock import Layout, Panel

from metanno import Column, DataRelation, DataWidgetFactory, RecipeTable

# Name, color and keyboard shortcut of each entity label
LABELS = {
    "tumeur": {"name": "Tumeur", "color": "rgb(255,200,206)", "shortcut": "t"},
    "tnm": {"name": "TNM", "color": "rgb(210,236,247)", "shortcut": "n"},
}
# Choices offered for the cancer site of a note
SITES = ["sein", "poumon", "autre"]


# Tables of the Store read by main, with the description and the words of
# their names shown when the view is opened from the sidebar
class Tables(TypedDict):
    notes: Annotated[DataFrame, RecipeTable("Notes with their text", keywords=["note", "doc"])]
    entities: Annotated[DataFrame, RecipeTable("Entities found in the notes", keywords=["entit"])]


def main(store: Tables, text_key: Annotated[str, Column("notes")] = "text"):
    """
    Review notes with their entities, in panels annotators can rearrange

    Parameters
    ----------
    text_key
        Column of the notes holding their text
    """
    factory = DataWidgetFactory(
        store,
        relations=[DataRelation("notes", "entities", on="note_id")],
    )

    # Notes to review, with a checkbox to mark them as reviewed
    notes = factory.create_table_widget(
        store_key="notes",
        primary_key="note_id",
        fields=[
            {"key": "note_id", "kind": "text"},
            {"key": "department", "kind": "text"},
            {"key": "reviewed", "kind": "boolean", "editable": True},
        ],
        style={"height": "100%"},
    )

    # Fields of the selected note, with buttons to move to the
    # previous or next note
    form = factory.create_form_widget(
        store_key="notes",
        primary_key="note_id",
        fields=[
            {"key": "site", "kind": "radio", "editable": True, "options": SITES},
            {"key": "reviewed", "kind": "boolean", "editable": True},
        ],
        add_navigation_buttons=True,
    )

    # Text of the selected note, its label toolbar, and a header showing
    # the ID of the note above the text
    text, toolbar = factory.create_text_widget(
        store_text_key="notes",
        store_spans_key="entities",
        text_key=text_key,
        text_primary_key="note_id",
        spans_primary_key="entity_id",
        fields=[
            {"key": "label", "kind": "select", "editable": True, "options": list(LABELS)},
            {"key": "negated", "kind": "boolean", "editable": True},
        ],
        labels=LABELS,
    )
    header = factory.create_selected_field_view(
        store_key="notes", shown_key="note_id", fallback="Note"
    )

    # Entities of the selected note
    entities = factory.create_table_widget(
        store_key="entities",
        primary_key="entity_id",
        fields=[
            {"key": "text", "kind": "text"},
            {"key": "label", "kind": "select", "editable": True, "options": list(LABELS)},
            {"key": "negated", "kind": "boolean", "editable": True},
            {"key": "checked", "kind": "boolean", "editable": True},
        ],
        style={"height": "100%"},
    )

    # Panels can be resized, dragged and stacked as tabs. default_config
    # gives their first arrangement, as rows and columns of tab groups
    layout = Layout(
        Panel(notes, key="Notes"),
        Panel(text, key="Text", header=header),
        Panel(Stack(form, Divider(), toolbar, spacing=1), key="Note"),
        Panel(entities, key="Entities"),
        default_config={
            "kind": "row",
            "children": [
                {"tabs": ["Notes"], "size": 20},
                {"tabs": ["Text"], "size": 50},
                {
                    "kind": "column",
                    "size": 30,
                    "children": [
                        {"tabs": ["Note"], "size": 50},
                        {"tabs": ["Entities"], "size": 50},
                    ],
                },
            ],
        },
    )
    # The status bar under the panels reports rejected edits and the
    # connection to Python
    return Stack(layout, factory.create_status_bar(), style={"height": "100%"})

As in review_entities, Metanno calls main with the Store of the file it opens, the same file the first cell loaded as store, and shows what main returns. Compared to that view, four things are new:

  • a form, create_form_widget, shows the fields of the selected note, with ← Previous and Next → buttons to go through the notes
  • a shortcut per label: select some text and press T to add a tumor, without reaching for the toolbar
  • a Layout of Panels, from pret_simple_dock, the Pret version of react-simple-dock, instead of a vertical Stack
  • Tables names the tables main reads, and text_key is a parameter with a default: they let you open the view on other tables from the sidebar, as shown below

Open it

from metanno import view_opener

view_opener(store, view="oncology_review")

The menu of the opener lists both of your views, oncology_review and review_entities, and opens the one you choose. Try the new interface:

  • drag the title of the Entities panel onto Notes to stack them as tabs, or resize the panels by dragging their borders
  • select poumon for note 4 in the form, tick reviewed, then click Next →
  • in the text, select a word and press T, click an entity and press Backspace to delete it, and press Left or Right to change notes

Opening the file from the sidebar now uses oncology_review, the last view you opened it with.

Open it on other tables

Annotators open oncology.pret with this view, but the same view also works on the notes and entities of a database, for instance to read what the pipeline found in a whole department before picking notes. Since main says which tables it reads with Tables, oncology_review shows under Recipes in the sidebar, after a click on the reload button ↻. Click it: the tab asks for a table for each key of Tables, then for a value for each parameter of main.

Try it on the tables of your annotation file, which the annotations source of the sidebar shows read only:

  • choose annotations.oncology/notes for notes. The tables whose name matches the keywords of the key come first, then those next to the tables already chosen
  • entities then takes annotations.oncology/entities, the table of the same file named as the key or one of its keywords, so you only pick one table per database
  • the text_key field lists the columns of the chosen notes, under the description given in the Parameters section of the docstring of main

Click Open view: the tab shows the notes and entities of the file with your interface, read only. The view menu of a source tab also lists your recipes, and choosing one that reads other tables shows the same choices.

Each parameter gets a field from its type and default: a checkbox for a bool, a list for a Literal, a text field otherwise, and the list of the columns of a table for a Column. A RecipeTable can also set max_rows, the rows loaded for its table instead of preview.rows in metanno.yaml, e.g. max_rows=50 for the notes and 5000 for their entities so that every entity of these notes shows.

Once the view looks right, Save view in the tab toolbar keeps it as a file such as metanno-home/views/oncology.yml, which then appears under Explore in the sidebar:

recipe: oncology_review
tables:
  notes:
    source: annotations
    table: oncology/notes
  entities:
    source: annotations
    table: oncology/entities

Each entry of tables becomes the table of the same key in the Store your view receives.

Choose how each field is shown

Each entry of fields picks a column, and kind picks how its values are shown and edited:

kind Shows Edited with
text the value as text a text box
boolean a checkbox a click
select the value a dropdown of options
radio the value radio buttons, in forms
autocomplete the value a searchable list of options, for long lists
hyperlink a link not editable

editable allows edits, and filterable, True by default, shows the filter box of a table column. options can also be a function of the row, to offer different choices for each note:

def site_options(note):
    """
    Offer the breast site first for radiology notes, mostly mammographies
    """
    return ["sein", "autre"] if note["department"] == "radiologie" else SITES


{"key": "site", "kind": "radio", "editable": True, "options": site_options}
Working faster in tables
  • click the ⋮ button of a column header to sort by that column, pin it to the left, or show and hide columns
  • drag a column header onto another one to move it
  • Ctrl+C copies the selected cells, Ctrl+Shift+C the selected rows with the column names
  • Ctrl+Z undoes the last change made in the view, Ctrl+Y or Ctrl+Shift+Z redoes it

On a Mac, use Cmd instead of Ctrl.

Add the annotation fields automatically

You added reviewed and site to oncology.pret by hand at the beginning of this tutorial. For the next notes you pick, Prepare annotation from Pick documents can add them for you: a recipe can define annotation_setup_code, a function returning code that the copied cell runs before saving the notes. Prepare annotation copies the table of a source tab, here clinic.notes, so this recipe must work with the notes table alone, unlike oncology_review which also needs the entities:

%%writefile ~/metanno-home/recipes/note_fields.py
from typing import Annotated, TypedDict

from pret import DataFrame
from pret_simple_dock import Layout, Panel

from metanno import DataWidgetFactory, RecipeTable

# Choices offered for the cancer site of a note
SITES = ["sein", "poumon", "autre"]


# The notes are the only table, so a source tab of notes can show this view
class Tables(TypedDict):
    notes: Annotated[DataFrame, RecipeTable("Notes with their text", keywords=["note", "doc"])]


def main(store: Tables):
    """
    Fill the fields of each note while reading it
    """
    factory = DataWidgetFactory(store)
    notes = factory.create_table_widget(
        store_key="notes",
        primary_key="note_id",
        fields=[
            {"key": "note_id", "kind": "text"},
            {"key": "department", "kind": "text"},
            {"key": "reviewed", "kind": "boolean", "editable": True},
        ],
        style={"height": "100%"},
    )
    # Without store_spans_key, the text widget only shows the text
    text, _ = factory.create_text_widget(
        store_text_key="notes", text_key="text", text_primary_key="note_id"
    )
    form = factory.create_form_widget(
        store_key="notes",
        primary_key="note_id",
        fields=[
            {"key": "site", "kind": "radio", "editable": True, "options": SITES},
            {"key": "reviewed", "kind": "boolean", "editable": True},
        ],
        add_navigation_buttons=True,
    )
    return Layout(
        Panel(notes, key="Notes"),
        Panel(text, key="Text"),
        Panel(form, key="Fields"),
        default_config={
            "kind": "row",
            "children": [
                {"tabs": ["Notes"], "size": 25},
                {"tabs": ["Text"], "size": 50},
                {"tabs": ["Fields"], "size": 25},
            ],
        },
    )


def annotation_setup_code(parameters):
    """
    Add the note fields to the notes copied by Prepare annotation
    """
    return 'tables["notes"] = tables["notes"].with_columns(reviewed=False, site="")'

Open the notes table of the clinic source in the sidebar, choose note_fields in the view menu of the tab, then click Copy code → Prepare annotation. The copied cell now adds the two columns before saving the notes:

tables = {
    'notes': DataFrame(notes),
}

tables["notes"] = tables["notes"].with_columns(reviewed=False, site="")

and the saved file opens with note_fields, ready for your annotators.

annotation_setup_code receives the parameters of the view: the arguments of main besides store, which a saved view file gives under parameters: and view_opener under parameters=. They let the setup code follow the options of the view. note_fields has none, so parameters is empty and the function ignores it.

Next

Notes are rarely annotated alone: Annotate across levels adds the patients above them, with decisions taken for a whole patient.