Using annotation data
This package contains a sole component named
dpa.DashPictureAnnotation. To use this
component, we can start from a minimal example:
import dash
from dash import html
import dash_picture_annotation as dpa
app = dash.Dash()
app.layout = html.Div(
    (
        dpa.DashPictureAnnotation(
            id="annotator",
            data=None,
            image="/site-address/to/an/image",
        ),
    )
)
Here, we let the data initialized by None, which means that no annotations are
defined when the component is loaded.
Users can interact with the annotator with
- Dragging and dropping for creating new annotation boxes;
 - Selecting an annotation box and specify its comment;
 - Selecting an annotation box, resizing, and relocating it;
 - Delete any annotation box.
 
Catch the update of the annotation data
Every time when the mouse is released, the annotation data will be updated. This update will fire a dash callback. Use the following codes to catch the callback.
app.layout = html.Div(
    (
        dpa.DashPictureAnnotation(id="annotator", ...),
        html.Div(id="output"),
    )
)
@app.callback(
    Output("output", "children"), Input("annotator", "data"), prevent_initial_call=False
)
def get_annotation(data: dpa.Annotations | None) -> Optional[str]:
    if data is None:
        return None
    return json.dumps(data)
Typically, the data returned by the annotator is typed by
dpa.Annotations. However
there are two exceptions:
data can be initialized by None or an unexpected value. Passing a data that
cannot be typed by dpa.Annotations to the
initialization of DashPictureAnnotation is allowed. In this case, the firstly loaded
value captured by the callback will be exactly the same as the value used for
initializing the component.
data can be updated by a callback using Output("annotator", "data"). In this case,
users may provide a value not typed by
dpa.Annotations. The subsequently fired
callback using this value as the input will be influenced.
Fortunately, the above example will not cause such issues because
- The 
datais initialized byNone, such a value is already correctly handled by the callback. - The 
datawould not be updated by any callback. In other words, it is only updated by user interactions. All updates fired by user interactions provide adatatyped bydpa.Annotations. 
Sanitization
The robustness of the callbacks capturing or updating the annotation can be improved by sanitization. Take the above codes as an example, we make a few modifications to let the data sanitized.
@app.callback(
    Output("output", "children"), Input("annotator", "data"), prevent_initial_call=False
)
def get_annotation(data: dpa.Annotations | None) -> Optional[str]:
    if data is None:
        return None
    if not dpa.is_annotations(data):
        return json.dumps(dpa.sanitize_data(data))
    return json.dumps(data)
This modification ensures that the returned value is always serializable even if
another callback modifies data without following the typehint dpa.Annotations.
The added lines provide the following functionalities:
- Check whether the 
datais formatted asdpa.Annotations. If not, usedpa.sanitize_data(...)to sanitize the data. - If the 
datais already formatted asdpa.Annotation, return the serialized JSON string directly. 
To verify this, users can modify the initialization and let data=1 which supposes
to be not JSON-serializable. With the sanitization, the callback stills works.
When defining a callback updating data, or saving the data as a file, the method
dpa.sanitize_data can be used, too. It can
exempt the inappropriate modifications made by users.
Add a reset button
The property data can be set by either the user interaction or the callback.
By adding another callback fired by a reset button, we can implement the feature of
clearing all annotations.
app.layout = html.Div(
    (
        html.Div(html.Button(id="btn-reset", children="Reset")),
        dpa.DashPictureAnnotation(id="annotator", ...),
        html.Div(id="output"),
    )
)
@app.callback(
    Output("annotator", "data"),
    Input("btn-reset", "n_clicks"),
    prevent_initial_call=True,
)
def reset_data(n_clicks: Optional[int]):
    if n_clicks:
        return dpa.sanitize_data([])
    return dash.no_update
In this example, we provides a button triggering the updating of the data. By using
dpa.sanitize_data, the updated value will
be sanitized and typed by dpa.Annotations.
Clicking this button will remove all annotations.
Get or change data by IDs
Each annotation box created by user interactions will by marked by a randomly-generated
ID. These unique IDs can be used for locating a specific annotation box. Suppose that
a new button is added for getting an annotation box for with a specific ID, and update
the annotation's comment. We can use
dpa.get_data_item to locate the item by
its ID.
app.layout = html.Div(
    (
        html.Div(html.Button(id="btn-getbyid", children="Get by ID")),
        dpa.DashPictureAnnotation(id="annotator", ...),
        html.Div(id="output"),
    )
)
@app.callback(
    Output("annotator", "data"),
    Input("btn-getbyid", "n_clicks"),
    State("annotator", "data"),
    prevent_initial_call=True,
)
def change_data_by_id(n_clicks: Optional[int], data: dpa.Annotations):
    if n_clicks:
        all_ids = dpa.get_all_ids(data)  # A sequence of IDs.
        data_item = dpa.get_data_item(data, all_ids[-1])
        data_item["comment"] = "new-comment"
        return data
    return dash.no_update
In this example, we modify the located data item. Any modification on the item will make the data changed in place. Therefore, returning the data directly will make the annotations changed.
Get or search data by comment
Another useful functionality is to query the data by the comment. Since different
annotation data items may have the same comment, the method
dpa.get_data_items is used for locating
all annotation items with a specific comment, for example
app.layout = html.Div(
    (
        html.Div(html.Button(id="btn-getbycomment", children="Get by comment")),
        dpa.DashPictureAnnotation(id="annotator", ...),
        html.Div(id="output"),
        html.Div(id="comment-ids"),
    )
)
@app.callback(
    Output("comment-ids", "children"),
    Input("btn-getbycomment", "n_clicks"),
    State("annotator", "data"),
    prevent_initial_call=True,
)
def get_data_by_comment(n_clicks: Optional[int], data: dpa.Annotations):
    if n_clicks:
        all_comments = dpa.get_all_comments(data)  # A frozen set of comments.
        data_items = dpa.get_data_items(data, next(iter(all_comments)))
        comment_ids = dpa.get_all_ids(data_items)  # A sequence of ids of `data_items`.
        return json.dumps(comment_ids)
    return dash.no_update
dpa.get_data_items can only locate the
items with a specific comment. There is another version used for searching the comments
by the regular expression, see
dpa.get_data_items_by_regex.
@app.callback(
    Output("comment-ids", "children"),
    Input("btn-getbycomment", "n_clicks"),
    State("annotator", "data"),
    prevent_initial_call=True,
)
def get_data_by_comment(n_clicks: Optional[int], data: dpa.Annotations):
    if n_clicks:
        all_comments = dpa.get_all_comments(data)  # A frozen set of comments.
        data_items = dpa.get_data_items_by_regex(data, r"^type-.*"))
        comment_ids = dpa.get_all_ids(data_items)  # A sequence of ids of `data_items`.
        return json.dumps(comment_ids)
    return dash.no_update
The above version will find all items with a comment starting with type-.
Certainly, changing data_items will make the data changed in place. The usages are
the same as the example of using get_data_item.