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