from typing import Optional
from decimal import Decimal
from validator_collection import validators
from highcharts_core import constants, errors
from highcharts_core.options.annotations.points import AnnotationPoint
from highcharts_core.decorators import class_sensitive, validate_types
from highcharts_core.metaclasses import HighchartsMeta
from highcharts_core.utility_classes.gradients import Gradient
from highcharts_core.utility_classes.patterns import Pattern
from highcharts_core.utility_classes.shadows import ShadowOptions
from highcharts_core.utility_classes.javascript_functions import CallbackFunction
[docs]class AnnotationLabelOptionAccessibility(HighchartsMeta):
"""Accessibility options applied to an annotation label."""
def __init__(self, **kwargs):
self._description = None
self.description = kwargs.get('description', None)
@property
def _dot_path(self) -> Optional[str]:
"""The dot-notation path to the options key for the current class.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return 'annotations.labelOptions.accessibility'
@property
def description(self) -> Optional[str]:
"""Description of an annotation label for screen readers and other assistive
technology.
:returns: Description of an annotation label
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._description
@description.setter
def description(self, value):
self._description = validators.string(value, allow_empty = True)
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
'description': as_dict.get('description', None)
}
return kwargs
def _to_untrimmed_dict(self, in_cls = None) -> dict:
untrimmed = {
'description': self.description
}
return untrimmed
[docs]class LabelOptions(HighchartsMeta):
"""Options applied to all annotation labels."""
def __init__(self, **kwargs):
self._accessibility = None
self._align = None
self._allow_overlap = None
self._background_color = None
self._border_color = None
self._border_radius = None
self._border_width = None
self._class_name = None
self._crop = None
self._distance = None
self._format = None
self._formatter = None
self._include_in_data_export = None
self._overflow = None
self._padding = None
self._shadow = None
self._shape = None
self._style = None
self._use_html = None
self._vertical_align = None
self._x = None
self._y = None
self.accessibility = kwargs.get('accessibility', None)
self.align = kwargs.get('align', None)
self.allow_overlap = kwargs.get('allow_overlap', None)
self.background_color = kwargs.get('background_color', None)
self.border_color = kwargs.get('border_color', None)
self.border_radius = kwargs.get('border_radius', None)
self.border_width = kwargs.get('border_width', None)
self.class_name = kwargs.get('class_name', None)
self.crop = kwargs.get('crop', None)
self.distance = kwargs.get('distance', None)
self.format = kwargs.get('format', None)
self.formatter = kwargs.get('formatter', None)
self.include_in_data_export = kwargs.get('include_in_data_export', None)
self.overflow = kwargs.get('overflow', None)
self.padding = kwargs.get('padding', None)
self.shadow = kwargs.get('shadow', None)
self.shape = kwargs.get('shape', None)
self.style = kwargs.get('style', None)
self.text = kwargs.get('text', None)
self.use_html = kwargs.get('use_html', None)
self.vertical_align = kwargs.get('vertical_align', None)
self.x = kwargs.get('x', None)
self.y = kwargs.get('y', None)
@property
def _dot_path(self) -> Optional[str]:
"""The dot-notation path to the options key for the current class.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return 'annotations.labelOptions'
@property
def accessibility(self) -> Optional[AnnotationLabelOptionAccessibility]:
"""Accessibility options applied to an annotation label.
:returns: Accessibility options applied to an annotation label.
:rtype: :class:`AnnotationLabelOptionAccessibility` or :obj:`None <python:None>`
"""
return self._accessibility
@accessibility.setter
@class_sensitive(AnnotationLabelOptionAccessibility)
def accessibility(self, value):
self._accessibility = value
@property
def align(self) -> Optional[str]:
"""The alignment of the annotation's label. Defaults to
``'center'``.
Accepts:
* ``'left'``
* ``'center'``
* ``'right'``
.. hint::
If right, the right side of the label should be touching the point.
:returns: The alignment of the annotation's label.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._align
@align.setter
def align(self, value):
if not value:
self._align = None
else:
value = validators.string(value, allow_empty = False)
value = value.lower()
if value not in ['left', 'center', 'right']:
raise errors.HighchartsValueError(f'align must be either "left", '
f'"center", or "right". Was: {value}')
self._align = value
@property
def allow_overlap(self) -> Optional[bool]:
"""If ``True``, annotation labels are allowed to overlap each other.
Defaults to ``False``.
:returns: Flag indicating whether to allow annotation labels to overlap.
:rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
"""
return self._allow_overlap
@allow_overlap.setter
def allow_overlap(self, value):
if value is None:
self._allow_overlap = None
else:
self._allow_overlap = bool(value)
@property
def background_color(self) -> Optional[str | Gradient | Pattern]:
"""The background color or gradient for the annotation's label. Defaults to
``'rgba(0, 0, 0, 0.75)'``.
:returns: The backgorund color for the annotation's label.
:rtype: :class:`str <python:str>`, :class:`Gradient`, :class:`Pattern``, or
:obj:`None <python:None>`
"""
return self._background_color
@background_color.setter
def background_color(self, value):
from highcharts_core import utility_functions
self._background_color = utility_functions.validate_color(value)
@property
def border_color(self) -> Optional[str]:
"""The border color for the annotation's label. Defaults to
``'#000000'``.
:returns: The border color for the annotation's label.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._border_color
@border_color.setter
def border_color(self, value):
self._border_color = validators.string(value, allow_empty = True)
@property
def border_radius(self) -> Optional[int | float | Decimal]:
"""The border radius (in pixels) applied to the annotation's label. Defaults to
``3``.
:returns: The border radius to apply to the annotation's label.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._border_radius
@border_radius.setter
def border_radius(self, value):
self._border_radius = validators.numeric(value, allow_empty = True)
@property
def border_width(self) -> Optional[int | float | Decimal]:
"""The border width (in pixels) applied to the annotation's label. Defaults to
``1``.
:returns: The border width to apply to the annotation's label.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._border_width
@border_width.setter
def border_width(self, value):
self._border_width = validators.numeric(value, allow_empty = True)
@property
def class_name(self) -> Optional[str]:
"""A classname to apply styling using CSS. Defaults to
``'highcharts-no-tooltip'``.
:returns: The classname to apply to enable styling via CSS.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._class_name
@class_name.setter
def class_name(self, value):
self._class_name = validators.string(value, allow_empty = True)
@property
def crop(self) -> Optional[bool]:
"""If ``True``, hide part of the annotation label that falls outside the plot
area. Defaults to ``False``.
:returns: Flag indicating whether to clip an annotation label that extends beyond
the plot area.
:rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
"""
return self._crop
@crop.setter
def crop(self, value):
if value is None:
self._crop = None
else:
self._crop = bool(value)
@property
def distance(self) -> Optional[int | float | Decimal]:
"""The label's distance in pixels from the point.
:returns: The label's distance in pixels from the point.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._distance
@distance.setter
def distance(self, value):
self._distance = validators.numeric(value,
allow_empty = True)
@property
def format(self) -> Optional[str]:
"""A format string to apply to the label.
.. seealso:
* :meth:`PlotOptions.series.data_labels`
.. warning::
If your format string begins with ``{`` and ends with ``}``, Highcharts
for Python may interpret it as a JavaScript or JSON object. To prevent this, please
add a ``$`` before the opening curly brace, like so: ``${value:.1f}``.
:returns: The format string to apply to the labels.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._format
@format.setter
def format(self, value):
self._format = validators.string(value, allow_empty = True)
@property
def formatter(self) -> Optional[CallbackFunction]:
"""JavaScript callback function to format the annotation's label.
Defaults to ``function () { return defined(this.y) ? this.y : 'Annotation label'; }``.
.. note::
If a :meth:`LabelOptions.format` or :meth:`LabelOptions.text` value are
specified, the formatter will be ignored.
.. hint::
In the callback function, the use of ``this`` refers to a point object.
:returns: A JavaScript callback function.
:rtype: :class:`CallbackFunction` or :obj:`None <python:None>`
"""
return self._formatter
@formatter.setter
@class_sensitive(CallbackFunction)
def formatter(self, value):
self._formatter = value
@property
def include_in_data_export(self) -> Optional[bool]:
"""If ``True``, the annotation is visible in the exported data table. Defaults to
``True``.
:returns: Flag indicating whether to include the annotation label in the chart's
exported data table.
:rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
"""
return self._include_in_data_export
@include_in_data_export.setter
def include_in_data_export(self, value):
if value is None:
self._include_in_data_export = None
else:
self._include_in_data_export = bool(value)
@property
def overflow(self) -> Optional[str]:
"""Configuration on how to handle an annotation label that overflows outside of
the plot area. Defaults to ``'justify'``.
Accepts:
* ``'justify'`` - which forces the label back into the plot area
* ``'none'`` - which applies no change
.. note::
The overflow treatment is also affected by the :meth:`LabelOptions.crop`
setting.
:returns: Configuration of overflow setting.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._overflow
@overflow.setter
def overflow(self, value):
value = validators.string(value, allow_empty = True)
if not value:
self._overflow = None
else:
value = value.lower()
if value not in ['justify', 'none']:
raise errors.HighchartsValueError(f'overflow accepts "justify" or "none".'
f' Was: {value}')
self._overflow = value
@property
def padding(self) -> Optional[int]:
"""The padding within the border box when either
:meth:`LabelOptions.border_width` or :meth:`LabelOptions.background_color` is set.
Defaults to ``5``.
:returns: The padding to apply to the annotation label.
:rtype: :class:`int <python:int>` or :obj:`None <python:None>`
"""
return self._padding
@padding.setter
def padding(self, value):
self._padding = validators.numeric(value, allow_empty = True)
@property
def shadow(self) -> Optional[bool | ShadowOptions]:
"""Configuration for the shadow to apply to the annotation box. Defaults to
``False``.
If ``False``, no shadow is applied.
:returns: The shadow configuration to apply or ``False``.
:rtype: :class:`bool <python:bool>` or :class:`ShadowOptions` or
:obj:`None <python:None>`
"""
return self._shadow
@shadow.setter
def shadow(self, value):
if value is None:
self._shadow = None
elif isinstance(value, bool) and value is False:
self._shadow = False
else:
value = validate_types(value,
types = ShadowOptions)
self._shadow = value
@property
def shape(self) -> Optional[str]:
"""The name of the symbol to use for the border around the label. Defaults to
``'callout'``.
Accepts:
* ``'connector'``
* ``'rect'``
* ``'circle'``
* ``'diamond'``
* ``'triangle'``
* ``'callout'``
:returns: The shape to use for the border around the label.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._shape
@shape.setter
def shape(self, value):
if not value:
self._shape = None
else:
value = validators.string(value, allow_empty = False)
value = value.lower()
if value not in ['callout',
'connector',
'rect',
'circle',
'diamond',
'triangle']:
raise errors.HighchartsValueError(f'shape expects a supported annotation '
f'label shape. Was: {value}')
self._shape = value
@property
def style(self) -> Optional[str | dict]:
"""CSS styling to apply to the annotation's label.
:rtype: :class:`str` or :obj:`None <python:None>`
"""
return self._style
@style.setter
def style(self, value):
try:
self._style = validators.dict(value, allow_empty = True)
except (ValueError, TypeError):
self._style = validators.string(value,
allow_empty = True,
coerce_value = True)
@property
def text(self) -> Optional[str]:
"""Alias for the :meth:`LabelOptions.format` property.
:returns: The format string to apply to the labels.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._format
@text.setter
def text(self, value):
self.format = value
@property
def use_html(self) -> Optional[bool]:
"""If ``True``, will use HTML to render the annotation's label. If ``False``, will
use SVG or WebGL as applicable.
Defaults to ``False``.
:returns: Flag indicating whether to render annotation labels using HTML.
:rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
"""
return self._use_html
@use_html.setter
def use_html(self, value):
if value is None:
self._use_html = None
else:
self._use_html = bool(value)
@property
def vertical_align(self) -> Optional[str]:
"""The vertical alignment of the annotation's label. Defaults to
``'bottom'``
Accepts:
* ``'bottom'``
* ``'middle'``
* ``'top'``
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._vertical_align
@vertical_align.setter
def vertical_align(self, value):
value = validators.string(value, allow_empty = True)
if not value:
self._vertical_align = None
else:
value = value.lower()
if value not in ['bottom', 'middle', 'top']:
raise errors.HighchartsValueError(f'vertical_align expects either "top", '
f'"middle", or "bottom". Was: {value}')
self._vertical_align = value
@property
def x(self) -> Optional[int]:
"""The x position offset of the label relative to the point. Defaults to
``0``.
.. note::
If a :meth:`LabelOptions.distance` is defined, the distance takes precedence.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._x
@x.setter
def x(self, value):
self._x = validators.numeric(value, allow_empty = True)
@property
def y(self) -> Optional[int]:
"""The y position offset of the label relative to the point. Defaults to
``-16``.
.. note::
If a :meth:`LabelOptions.distance` is defined, the distance takes precedence.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._y
@y.setter
def y(self, value):
self._y = validators.numeric(value, allow_empty = True)
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
'accessibility': as_dict.get('accessibility', None),
'align': as_dict.get('align', None),
'allow_overlap': as_dict.get('allowOverlap', None),
'background_color': as_dict.get('backgroundColor', None),
'border_color': as_dict.get('borderColor', None),
'border_radius': as_dict.get('borderRadius', None),
'border_width': as_dict.get('borderWidth', None),
'class_name': as_dict.get('className', None),
'crop': as_dict.get('crop', None),
'distance': as_dict.get('distance', None),
'format': as_dict.get('format', None),
'formatter': as_dict.get('formatter', None),
'include_in_data_export': as_dict.get('includeInDataExport', None),
'overflow': as_dict.get('overflow', None),
'padding': as_dict.get('padding', None),
'shadow': as_dict.get('shadow', None),
'shape': as_dict.get('shape', None),
'style': as_dict.get('style', None),
'text': as_dict.get('text', None),
'use_html': as_dict.get('useHTML', None),
'vertical_align': as_dict.get('verticalAlign', None),
'x': as_dict.get('x', None),
'y': as_dict.get('y', None),
}
return kwargs
def _to_untrimmed_dict(self, in_cls = None) -> dict:
untrimmed = {
'accessibility': self.accessibility,
'align': self.align,
'allowOverlap': self.allow_overlap,
'backgroundColor': self.background_color,
'borderColor': self.border_color,
'borderRadius': self.border_radius,
'borderWidth': self.border_width,
'className': self.class_name,
'crop': self.crop,
'distance': self.distance,
'format': self.format,
'formatter': self.formatter,
'includeInDataExport': self.include_in_data_export,
'overflow': self.overflow,
'padding': self.padding,
'shadow': self.shadow,
'shape': self.shape,
'style': self.style,
'text': self.text,
'useHTML': self.use_html,
'verticalAlign': self.vertical_align,
'x': self.x,
'y': self.y
}
return untrimmed
[docs]class AnnotationLabel(LabelOptions):
"""Configuration for an annotation label applied to a specific point.
Used to override the global settings configured in :class:`LabelOptions` and applied
via :meth:`Annotation.label_options`.
"""
def __init__(self, **kwargs):
self._point = None
self.point = kwargs.get('point', None)
super().__init__(**kwargs)
@property
def point(self) -> Optional[str | AnnotationPoint]:
"""Determines the point to which the label will be connected.
It can be either the ID of the point which exists in the series, or a new point
with defined x, y properties and optionally axes.
:rtype: :class:`str <python:str>` or :class:`AnnotationPoint` or
:obj:`None <python:None>`
:raises HighchartsValueError: if cannot resolve the value to an allowed type
"""
return self._point
@point.setter
def point(self, value):
if not value:
self._point = None
elif isinstance(value, AnnotationPoint):
self._point = value
elif isinstance(value, str):
try:
self._point = AnnotationPoint.from_json(value)
except ValueError:
self._point = validators.string(value)
elif isinstance(value, dict):
self._point = AnnotationPoint.from_dict(value)
else:
raise errors.HighchartsValueError('Unable to resolve the value supplied to a '
'supported type.')
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
# from LabelOptions
'accessibility': as_dict.get('accessibility', None),
'align': as_dict.get('align', None),
'allow_overlap': as_dict.get('allowOverlap', None),
'background_color': as_dict.get('backgroundColor', None),
'border_color': as_dict.get('borderColor', None),
'border_radius': as_dict.get('borderRadius', None),
'border_width': as_dict.get('borderWidth', None),
'class_name': as_dict.get('className', None),
'crop': as_dict.get('crop', None),
'distance': as_dict.get('distance', None),
'format': as_dict.get('format', None),
'formatter': as_dict.get('formatter', None),
'include_in_data_export': as_dict.get('includeInDataExport', None),
'overflow': as_dict.get('overflow', None),
'padding': as_dict.get('padding', None),
'shadow': as_dict.get('shadow', None),
'shape': as_dict.get('shape', None),
'style': as_dict.get('style', None),
'text': as_dict.get('text', None),
'use_html': as_dict.get('useHTML', None),
'vertical_align': as_dict.get('verticalAlign', None),
'x': as_dict.get('x', None),
'y': as_dict.get('y', None),
# from AnnotationLabel
'point': as_dict.get('point', None),
}
return kwargs
def _to_untrimmed_dict(self, in_cls = None) -> dict:
untrimmed = {
'point': self.point
}
parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {}
for key in parent_as_dict:
untrimmed[key] = parent_as_dict[key]
return untrimmed
__all__ = [
'LabelOptions',
'AnnotationLabelOptionAccessibility',
'ShadowOptions',
'AnnotationLabel'
]