Source code for highcharts_core.options.annotations.shape_options

import os
from typing import Optional, List
from decimal import Decimal

from validator_collection import validators, checkers

from highcharts_core import constants, errors
from highcharts_core.decorators import validate_types
from highcharts_core.utility_classes import Gradient, Pattern
from highcharts_core.options.annotations.points import AnnotationPoint
from highcharts_core.metaclasses import HighchartsMeta
from highcharts_core.utility_classes.javascript_functions import CallbackFunction
from highcharts_core.utility_functions import validate_color


class ShapeOptionsBase(HighchartsMeta):
    """Base set of options applied to all annotation shapes."""

    def __init__(self, **kwargs):
        self._dash_style = None
        self._fill = None
        self._height = None
        self._ry = None
        self._snap = None
        self._src = None
        self._stroke = None
        self._stroke_width = None
        self._x_axis = None
        self._y_axis = None

        self.dash_style = kwargs.get('dash_style', None)
        self.fill = kwargs.get('fill', None)
        self.height = kwargs.get('height', None)
        self.ry = kwargs.get('ry', None)
        self.snap = kwargs.get('snap', None)
        self.src = kwargs.get('src', None)
        self.stroke = kwargs.get('stroke', None)
        self.stroke_width = kwargs.get('stroke_width', None)
        self.x_axis = kwargs.get('x_axis', None)
        self.y_axis = kwargs.get('y_axis', None)

    @property
    def dash_style(self) -> Optional[str]:
        """Name of the dash style to use for the shape's stroke.

        Accepts the following values:

          * ``'Solid'``
          * ``'ShortDash'``
          * ``'ShortDot'``
          * ``'ShortDashDot'``
          * ``'ShortDashDotDot'``
          * ``'Dot'``
          * ``'Dash'``
          * ``'LongDash'``
          * ``'DashDot'``
          * ``'LongDashDot'``
          * ``'LongDashDotDot'``

        :returns: The name of the dash style to apply to the shape's stroke.
        :rtype: :class:`str <python:str>` or :obj:`None <python:None>`
        """
        return self._dash_style

    @dash_style.setter
    def dash_style(self, value):
        value = validators.string(value, allow_empty = True)
        if not value:
            self._dash_style = None
        else:
            if value not in constants.SHAPES_ALLOWED_DASH_STYLES:
                raise errors.HighchartsValueError(f'dash_style expects a supported value.'
                                                  f' Received: {value}')
            self._dash_style = value

    @property
    def fill(self) -> Optional[str | Gradient | Pattern]:
        """The color of the shape's fill. Defaults to ``'rgba(0, 0, 0, 0.75)'``.

        :rtype: :class:`str <python:str>` (for colors), :class:`Gradient` for gradients,
          :class:`Pattern` for pattern definitions, or :obj:`None <python:None>`
        """
        return self._fill

    @fill.setter
    def fill(self, value):
        self._fill = validate_color(value)

    @property
    def height(self) -> Optional[int | float | Decimal]:
        """The height of the shape in pixels.

        :rtype: numeric or :obj:`None <python:None>`
        """
        return self._height

    @height.setter
    def height(self, value):
        self._height = validators.numeric(value, allow_empty = True)

    @property
    def r(self) -> Optional[int | float | Decimal]:
        """The radius of the shape in pixels. Defaults to ``0``.

        :rtype: numeric or :obj:`None <python:None>`
        """
        return self._r

    @r.setter
    def r(self, value):
        self._r = validators.numeric(value, allow_empty = True)

    @property
    def ry(self) -> Optional[int | float | Decimal]:
        """The radius of the shape along the vertical dimension. Used to draw ellipses.

        :rtype: numeric or :obj:`None <python:None>`
        """
        return self._ry

    @ry.setter
    def ry(self, value):
        self._ry = validators.numeric(value, allow_empty = True)

    @property
    def snap(self) -> Optional[int | float | Decimal]:
        """Defines additional snapping area around an annotation making this annotation
        to focus. Defined in pixels.

        Defaults to ``2``.

        :rtype: numeric or :obj:`None <python:None>`
        """
        return self._snap

    @snap.setter
    def snap(self, value):
        self._snap = validators.numeric(value, allow_empty = True)

    @property
    def src(self) -> Optional[str]:
        """The URL for an image to use as the annotation shape.

        .. note::

          :meth:`ShapeOptions.type` has to be set to ``'image'``.

        :rtype: :class:`str <python:str>` or :obj:`None <python:None>`

        :raises HighchartsValueError: if a value is supplied that is not a URL or not
          path-like

        """
        return self._src

    @src.setter
    def src(self, value):
        if not value:
            self._src = None
        else:
            try:
                self._src = validators.url(
                    value, allow_special_ips=os.getenv("HCP_ALLOW_SPECIAL_IPS", False)
                )
            except ValueError:
                try:
                    self._src = validators.path(value)
                except ValueError:
                    raise errors.HighchartsValueError(f'value provided ({value}) not a '
                                                      f'valid URL or path')

    @property
    def stroke(self) -> Optional[str]:
        """The color of the shape's stroke. Defaults to
        ``'rgba(0, 0, 0, 0.75)'``.

        :rtype: :class:`str <python:str>` or :obj:`None <python:None>`
        """
        return self._stroke

    @stroke.setter
    def stroke(self, value):
        self._stroke = validators.string(value, allow_empty = True)

    @property
    def stroke_width(self) -> Optional[int | float | Decimal]:
        """The pixel stroke width of the shape. Defaults to
        ``1``.

        :rtype: numeric or :obj:`None <python:None>`
        """
        return self._stroke_width

    @stroke_width.setter
    def stroke_width(self, value):
        self._stroke_width = validators.numeric(value, allow_empty = True)

    @property
    def type(self) -> Optional[str]:
        """The type of the shape. Defaults to ``'rect'``.

        Accepts:

          * ``'rect'``
          * ``'circle'``
          * ``'ellipse'``
          * ``'image'``

        :returns: :class:`str <python:str>` or :obj:`None <python:None>`

        :raises HighchartsValueError: if type not supported
        """
        return self._type

    @type.setter
    def type(self, value):
        value = validators.string(value, allow_empty = True)
        if not value:
            self._type = None
        else:
            value = value.lower()
            if value not in ['rect', 'circle', 'ellipse']:
                raise errors.HighchartsValueError(f'ShapeOptions.type accepts either '
                                                  f'"rect", "circle", "image", or '
                                                  f'"ellipse". Received: {value}')
            self._type = value

    @property
    def width(self) -> Optional[int | float | Decimal]:
        """The width of the shape, expressed in pixels.

        :rtype: numeric or :obj:`None <python:None>`
        """
        return self._width

    @width.setter
    def width(self, value):
        self._width = validators.numeric(value, allow_empty = True)

    @property
    def x_axis(self) -> Optional[int]:
        """The X-Axis index to which the points should be attached.

        .. note::

          Used for the ``ellipse`` :meth:`type <ShapeOptions.type>`

        :rtype: :class:`int <python:int>` or :obj:`None <python:None>`

        :raises ValueError: if set to a negative value

        """
        return self._x_axis

    @x_axis.setter
    def x_axis(self, value):
        self._x_axis = validators.integer(value,
                                          allow_empty = True,
                                          minimum = 0,
                                          coerce_value = True)

    @property
    def y_axis(self) -> Optional[int]:
        """The Y-Axis index to which the points should be attached.

        .. note::

          Used for the ``ellipse`` :meth:`type <ShapeOptions.type>`

        :rtype: :class:`int <python:int>` or :obj:`None <python:None>`

        :raises ValueError: if set to a negative value
        """
        return self._y_axis

    @y_axis.setter
    def y_axis(self, value):
        self._y_axis = validators.integer(value,
                                          allow_empty = True,
                                          minimum = 0,
                                          coerce_value = True)

    @classmethod
    def _get_kwargs_from_dict(cls, as_dict):
        kwargs = {
            'dash_style': as_dict.get('dashStyle', None),
            'fill': as_dict.get('fill', None),
            'height': as_dict.get('height', None),
            'ry': as_dict.get('ry', None),
            'snap': as_dict.get('snap', None),
            'src': as_dict.get('src', None),
            'stroke': as_dict.get('stroke', None),
            'stroke_width': as_dict.get('strokeWidth', None),
            'x_axis': as_dict.get('xAxis', None),
            'y_axis': as_dict.get('yAxis', None)
        }
        return kwargs

    def _to_untrimmed_dict(self, in_cls = None) -> dict:
        untrimmed = {
            'dashStyle': self.dash_style,
            'fill': self.fill,
            'height': self.height,
            'ry': self.ry,
            'snap': self.snap,
            'src': self.src,
            'stroke': self.stroke,
            'strokeWidth': self.stroke_width,
            'xAxis': self.x_axis,
            'yAxis': self.y_axis
        }

        return untrimmed


[docs]class ShapeOptions(ShapeOptionsBase): """Global options applied to all annotation shapes.""" def __init__(self, **kwargs): self._r = None self._type = None self._width = None self.r = kwargs.get('r', None) self.type = kwargs.get('type', None) self.width = kwargs.get('width', None) super().__init__(**kwargs) @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.shapeOptions' @property def r(self) -> Optional[int | float | Decimal]: f"""The radius of the shape in pixels. Defaults to {constants.DEFAULT_SHAPES_R}. :rtype: numeric or :obj:`None <python:None>` """ return self._r @r.setter def r(self, value): self._r = validators.numeric(value, allow_empty = True) @property def type(self) -> Optional[str]: f"""The type of the shape. Defaults to ``{constants.DEFAULT_SHAPES_TYPE}``. Accepts: * ``'rect'`` * ``'circle'`` * ``'ellipse'`` * ``'image'`` :returns: :class:`str <python:str>` or :obj:`None <python:None>` :raises HighchartsValueError: if type not supported """ return self._type @type.setter def type(self, value): value = validators.string(value, allow_empty = True) if not value: self._type = None else: value = value.lower() if value not in ['rect', 'circle', 'ellipse']: raise errors.HighchartsValueError(f'ShapeOptions.type accepts either ' f'"rect", "circle", "image", or ' f'"ellipse". Received: {value}') self._type = value @property def width(self) -> Optional[int | float | Decimal]: """The width of the shape, expressed in pixels. :rtype: numeric or :obj:`None <python:None>` """ return self._width @width.setter def width(self, value): self._width = validators.numeric(value, allow_empty = True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { 'dash_style': as_dict.get('dashStyle', None), 'fill': as_dict.get('fill', None), 'height': as_dict.get('height', None), 'r': as_dict.get('r', None), 'ry': as_dict.get('ry', None), 'snap': as_dict.get('snap', None), 'src': as_dict.get('src', None), 'stroke': as_dict.get('stroke', None), 'stroke_width': as_dict.get('strokeWidth', None), 'type': as_dict.get('type', None), 'width': as_dict.get('width', None), 'x_axis': as_dict.get('xAxis', None), 'y_axis': as_dict.get('yAxis', None) } return kwargs def _to_untrimmed_dict(self, in_cls = None) -> dict: untrimmed = { 'dashStyle': self.dash_style, 'fill': self.fill, 'height': self.height, 'r': self.r, 'ry': self.ry, 'snap': self.snap, 'src': self.src, 'stroke': self.stroke, 'strokeWidth': self.stroke_width, 'type': self.type, 'width': self.width, 'xAxis': self.x_axis, 'yAxis': self.y_axis } return untrimmed
[docs]class AnnotationShape(ShapeOptions): """Configuration for an annotation shape applied to a specific point. Used to override the global settings configured in :class:`ShapeOptions` and applied via :meth:`Annotation.shape_options`. """ def __init__(self, **kwargs): self._marker_end = None self._marker_start = None self._point = None self._points = None self.marker_end = kwargs.get('marker_end', None) self.marker_start = kwargs.get('marker_start', None) self.point = kwargs.get('point', None) self.points = kwargs.get('points', None) super().__init__(**kwargs) @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.shapes' @property def marker_end(self) -> Optional[str]: """ID of the marker which will be drawn at the final vertex of the path. .. note:: Custom markers can be defined in the :meth:`Options.defs` property. :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ return self._marker_end @marker_end.setter def marker_end(self, value): self._marker_end = validators.string(value, allow_empty = True) @property def marker_start(self) -> Optional[str]: """ID of the marker which will be drawn at the first vertex of the path. .. note:: Custom markers can be defined in the :meth:`Options.defs` property. :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ return self._marker_start @marker_start.setter def marker_start(self, value): self._marker_start = validators.string(value, allow_empty = True) @property def point(self) -> Optional[str | AnnotationPoint]: """Determines the point to which the shape 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.') @property def points(self) -> Optional[List[CallbackFunction | AnnotationPoint | str]]: """An array of points for the shape or a JavaScript callback function that returns that shape point. .. note:: This option is available for shapes which can use multiple points such as path. A point can be either a :class:`AnnotationPoint` object or a point's id. :rtype: :class:`list <python:list>` of :class:`CallbackFunction` or :class:`str <python:str>` or :class:`AnnotationPoint` or :class:`str <python:str>`, OR :obj:`None <python:None>` """ return self._points @points.setter def points(self, value): if not value: self._points = None elif checkers.is_iterable(value): processed_value = [] for item in value: try: item = validate_types(item, types = AnnotationPoint) except ValueError: try: item = validate_types(item, types = CallbackFunction) except (ValueError, TypeError): try: item = validators.string(item, allow_empty = False) except ValueError: raise errors.HighchartsValueError( f'points expects ' f'AnnotationPoint, ' f'CallbackFunction, or str' f'instances. Received: ' f'{item.__class__.__name__}' ) processed_value.append(item) self._points = [x for x in processed_value] else: raise errors.HighchartsValueError('Unable to resolve the value to a ' 'supported type.') @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { # from ShapeOptions 'dash_style': as_dict.get('dashStyle', None), 'fill': as_dict.get('fill', None), 'height': as_dict.get('height', None), 'r': as_dict.get('r', None), 'ry': as_dict.get('ry', None), 'snap': as_dict.get('snap', None), 'src': as_dict.get('src', None), 'stroke': as_dict.get('stroke', None), 'stroke_width': as_dict.get('strokeWidth', None), 'type': as_dict.get('type', None), 'width': as_dict.get('width', None), 'x_axis': as_dict.get('xAxis', None), 'y_axis': as_dict.get('yAxis', None), # from AnnotationShape 'marker_end': as_dict.get('markerEnd', None), 'marker_start': as_dict.get('markerStart', None), 'point': as_dict.get('point', None), 'points': as_dict.get('points', None), } return kwargs def _to_untrimmed_dict(self, in_cls = None) -> dict: untrimmed = { 'markerEnd': self.marker_end, 'markerStart': self.marker_start, 'point': self.point, 'points': self.points, } parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] return untrimmed
__all__ = [ 'ShapeOptions', 'AnnotationShape', 'ShapeOptionsBase' ]