Source code for visualization_toolkit.helpers.plotly.charts.core.annotation

from typing import Literal, Tuple, Optional
from dataclasses import dataclass, field
from datetime import date, datetime

import plotly.graph_objects as go
from copy import deepcopy

from visualization_toolkit.helpers.plotly.colors import resolve_color
from visualization_toolkit.helpers.plotly.theme import (
    ANNOTATION_FONT_COLOR,
    ANNOTATION_LINE_WIDTH,
    FONT_FAMILY,
    ANNOTATION_FONT_SIZE,
)


@dataclass
class Annotation:
    """
    Define text-based annotations to be rendered on charts. Depending on the `axis_location` set,
    the annotation will include a horizontal or vertical line. The position of the annotation is
    controlled by the `value` which equals the value on the corresponding the axis where an annotation is desired.

    The annotation uses either the Plotly ``hline``, ``vline``, or ``annotation`` object depending on the `axis_location` specified.

    :param text: Text that will be rendered on the annotation
    :param value: The value on the corresponding axis that the annotation will be placed. If no `axis_location` is set, a floating annotation position can be specified by a tuple (x,y) pair for this argument.
    :param axis_location: Specify whether the annotation should be oriented by the `x` axis, `y1` / `y2` axis, or if `None` a floating annotation is used. Default `None`.
    :param position: The relative position of the annotation text, relevant to the drawn line for the annotation. If a floating annotation is desired, this value has no effect. Default is top-right of the annotation line.
    :param color: The color of the annotation line and text. If `None`, the standard theme is used. Default `None`.
    :param line_shape: Control if the annotation line is drawn with dashes, dots, or both. Default is dot.
    :param extra_options: Additional options to style the underlying Plotly figure object.

    Examples
    ^^^^^^^^^^^^^
    .. code-block:: python
        :caption: Add a vertical annotation on a line chart. The annotation would be placed where 1970 falls on the x-axis.

        from visualization_toolkit.helpers.plotly import chart, axis, series, annotation

        fig = chart(
            df,
            x_axis=axis(
                column_name="year",
                label="Year",
            ),
            chart_series=[
                series(
                    column_name="lifeExp",
                    category_column_name="country",
                    location="y1",
                ),
            ],
            y1_axis=axis(
                label="Life Expectancy",
                axis_type="number",
            ),
            annotations=[
                annotation(
                    text="Example Annotation",
                    value=1970,
                    axis_location="x",
                ),
            ],
        )

        display(fig)

    """

    text: str
    value: int | float | date | datetime | Tuple[
        int | float | date | datetime, int | float | date | datetime
    ]
    axis_location: Optional[Literal["x", "y1", "y2"]] = None
    position: Literal[
        "bottom right",
        "bottom center",
        "bottom left",
        "middle right",
        "middle center",
        "middle left",
        "top right",
        "top center",
        "top left",
    ] = "top right"
    color: str = None
    line_shape: Optional[Literal["dash", "dot", "dashdot"]] = "dot"
    extra_options: dict = field(default_factory=dict)

    def register_in_figure(self, figure: go.Figure, y_range: tuple = None):
        match self.axis_location:
            case "x":
                match self.position.split(" ")[0]:
                    case "top":
                        y = y_range[1]
                    case "middle":
                        y = (y_range[1] + y_range[0]) / 2
                    case "bottom":
                        y = y_range[0]

                extra_options = deepcopy(self.extra_options)
                if self.axis_location is None:
                    extra_options["arrowcolor"] = self.resolved_font_color

                figure.add_vline(**self.resolved_annotation_configuration)
                figure.add_annotation(
                    **{
                        "x": self.value,
                        "y": y,
                        "text": self.text,
                        "font": {
                            "color": self.resolved_font_color,
                            "family": FONT_FAMILY,
                            "size": ANNOTATION_FONT_SIZE,
                        },
                        "showarrow": False,
                        "xanchor": self.position.split(" ")[1],
                        "yanchor": self.position.split(" ")[0],
                    }
                    | extra_options
                )

            case "y1" | "y2":
                figure.add_hline(**self.resolved_annotation_configuration)

            case _:
                figure.add_annotation(**self.resolved_annotation_configuration)

    @property
    def resolved_font_color(self) -> str:
        if self.color is not None:
            return resolve_color(self.color)

        return ANNOTATION_FONT_COLOR

    @property
    def resolved_line_color(self) -> str:
        if self.color is not None:
            return resolve_color(self.color)

        return ANNOTATION_FONT_COLOR

    @property
    def resolved_annotation_configuration(self) -> dict:
        match self.axis_location:
            case "x":
                return {
                    "x": self.value,
                    "line_dash": self.line_shape,
                    "line": {
                        "color": self.resolved_line_color,
                        "width": ANNOTATION_LINE_WIDTH,
                    },
                }

            case "y1" | "y2":
                return {
                    "y": self.value,
                    "yref": self.axis_location,
                    "line_dash": self.line_shape,
                    "line": {
                        "color": self.resolved_line_color,
                        "width": ANNOTATION_LINE_WIDTH,
                    },
                    "annotation_text": self.text,
                    "annotation_position": self.position,
                    "annotation_font_color": self.resolved_font_color,
                } | self.extra_options

            case _:
                return {
                    "x": self.value[0],
                    "y": self.value[1],
                    "text": self.text,
                    "font": {
                        "color": self.resolved_font_color,
                    },
                    "showarrow": False,
                } | self.extra_options


[docs] annotation = Annotation