from decimal import Decimal
from typing import Optional, List, Any
from collections import UserDict
from validator_collection import validators, checkers
from highcharts_core.options.series.data.connections import *
from highcharts_maps import constants, errors
from highcharts_maps.decorators import class_sensitive
from highcharts_maps.utility_classes.gradients import Gradient
from highcharts_maps.utility_classes.patterns import Pattern
from highcharts_maps.utility_classes.markers import FlowmapMarker
[docs]class FlowmapData(WeightedConnectionData):
"""Variant of :class:`ConnectionData` that also applies a ``weight`` to the
connection."""
def __init__(self, **kwargs):
self._curve_factor = None
self._fill_color = None
self._fill_opacity = None
self._grow_towards = None
self._line_width = None
self._marker_end = None
self._opacity = None
self._weight = None
self.curve_factor = kwargs.get('curve_factor', None)
self.fill_color = kwargs.get('fill_color', None)
self.fill_opacity = kwargs.get('fill_opacity', None)
self.grow_towards = kwargs.get('grow_towards', None)
self.line_width = kwargs.get('line_width', None)
self.marker_end = kwargs.get('marker_end', None)
self.opacity = kwargs.get('opacity', None)
self.weight = kwargs.get('weight', None)
super().__init__(**kwargs)
@property
def curve_factor(self) -> Optional[int | float | Decimal]:
"""The amount by which to curve the lines on a flowmap. Higher numbers makes the links
more curved, while a value of ``0`` makes the lines straight. Defaults to
:obj:`None <python:None>`.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._curve_factor
@curve_factor.setter
def curve_factor(self, value):
self._curve_factor = validators.numeric(value, allow_empty = True)
@property
def fill_color(self) -> Optional[str | Gradient | Pattern | constants.EnforcedNullType]:
"""Fill color or gradient for the area. When :class:`EnforcedNullType`, the
series' color is used with the series'
:meth:`.fill_opacity <FlowmapOptions.fill_opacity>`.
:rtype: :obj:`None <python:None>`, :class:`Gradient`, :class:`Pattern`, or
:class:`EnforcedNullType`
"""
return self._fill_color
@fill_color.setter
def fill_color(self, value):
from highcharts_maps.utility_functions import validate_color
self._fill_color = validate_color(value)
@property
def fill_opacity(self) -> Optional[int | float | Decimal]:
"""Fill opacity for the area. Defaults to ``0.5``.
When you set an explicit :meth:`fill_color <FlowmapOptions.fill_color>`, the
``fill_opacity`` is not applied. Instead, you should define the opacity in the
:meth:`fill_color <FlowmapOptions.fill_color>` with an rgba color definition.
The ``fill_opacity`` setting, also the default setting, overrides the alpha
component of the color setting.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._fill_opacity
@fill_opacity.setter
def fill_opacity(self, value):
self._fill_opacity = validators.numeric(value,
allow_empty = True,
minimum = 0)
@staticmethod
def _validate_coordinates(value) -> Optional[List[int | float | Decimal]]:
"""Processes ``value`` to confirm that they contain valid coordinates, expressed as a ``longitude, latitude``.
:param value: The value to validate.
:type value: any
:returns: A 2-member :class:`list <python:list>` of the form ``[longitude, latitude]``, or
:obj:`None <python:None>`
:rtype: :class:`list <python:list>` of numeric, or :obj:`None <python:None>`
"""
if value is None:
return None
elif checkers.is_string(value):
return value
elif checkers.is_iterable(value, forbid_literals = (str, bytes, dict, UserDict)):
if len(value) != 2:
raise errors.HighchartsValueError(f'.from expects coordinates expressed in longitude, latitude. '
f'Received a set of coordinates with {len(value)} members.')
return [validators.numeric(x, allow_empty = False) for x in value]
elif isinstance(value, (dict, UserDict)):
missing_keys = []
reformed_value = []
if 'lon' in value:
reformed_value.append(value.get('lon', None))
elif 'longitude' in value:
reformed_value = reformed_value.append(value.get('longitude', None))
else:
missing_keys.append('longitude')
if 'lat' in value:
reformed_value.append(value.get('lat', None))
elif 'latitude' in value:
reformed_value.append(value.get('latitude', None))
else:
missing_keys.append('latitude')
if missing_keys:
raise errors.HighchartsValueError(f'.from expects coordinates with both a longitude and a latitude. '
f'Value received was missing: {missing_keys}')
return [validators.numeric(x, allow_empty = False) for x in reformed_value]
else:
missing_keys = []
reformed_value = []
if hasattr(value, 'lon'):
reformed_value.append(getattr(value, 'lon', None))
elif hasattr(value, 'longitude'):
reformed_value.append(getattr(value, 'longitude', None))
else:
missing_keys.append('longitude')
if hasattr(value, 'lat'):
reformed_value.append(getattr(value, 'lat', None))
elif hasattr(value, 'latitude'):
reformed_value.append(getattr(value, 'latitude', None))
else:
missing_keys.append('latitude')
if missing_keys:
raise errors.HighchartsValueError(f'value is expected to a set of coordinates, expressed either '
f'as an ID (string), a 2-member iterable of numerical coordinates '
f'of the form [longitude, latitude], a dict with keys '
f'"longitude"/"lon" and "latitude"/"lat", or an object with '
f'properties "longitude"/"lon" and "latitude"/"lat". Value received'
f' did not match any of these, and was missing keys/properties: '
f'{missing_keys}')
return [validators.numeric(x, allow_empty = False) for x in reformed_value]
@property
def from_(self) -> Optional[str | List[int | float | Decimal]]:
"""The coordinates for the link's origin point. Accepts either:
* an ID referencing a map point holding coordinates of the link origin
* coordinates expressed as a 2-member array of the form ``[longitude, latitude]``,
* :class:`dict <python:dict>` with keys ``'lon'/'longitude'`` and ``'lat'/'latitude'``, or
* arbitrary object with ``'lon'/'longitude'`` and ``'lat'/'latitude'`` properties.
Defaults to :obj:`None <python:None>`.
:rtype: :class:`str <python:str>` or iterable of coordinates or :obj:`None <python:None>`
"""
return self._from_
@from_.setter
def from_(self, value):
self._from_ = self._validate_coordinates(value)
@property
def to(self) -> Optional[str | List[int | float | Decimal]]:
"""The coordinates for the link's destination. Accepts either:
* an ID referencing a map point holding coordinates of the link origin
* coordinates expressed as a 2-member array of the form ``[longitude, latitude]``,
* :class:`dict <python:dict>` with keys ``'lon'/'longitude'`` and ``'lat'/'latitude'``, or
* arbitrary object with ``'lon'/'longitude'`` and ``'lat'/'latitude'`` properties.
Defaults to :obj:`None <python:None>`.
:rtype: :class:`str <python:str>` or iterable of coordinates or :obj:`None <python:None>`
"""
return self._to
@to.setter
def to(self, value):
self._to = self._validate_coordinates(value)
@property
def grow_towards(self) -> Optional[bool]:
"""If ``True``, the line will grow as it approaches its destination. Defaults to :obj:`None <python:None>`.
:rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
"""
return self._grow_towards
@grow_towards.setter
def grow_towards(self, value):
if value is None:
self._grow_towards = None
else:
self._grow_towards = bool(value)
@property
def line_width(self) -> Optional[int | float | Decimal]:
"""Pixel width of the graph line. Defaults to :obj:`None <python:None>`.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._line_width
@line_width.setter
def line_width(self, value):
self._line_width = validators.numeric(value,
allow_empty = True,
minimum = 0)
@property
def marker_end(self) -> Optional[FlowmapMarker]:
"""If enabled, creates an arrow symbol indicating the direction of the flow at the flow's destination.
.. warning::
Setting/enabling this property in the
:class:`FlowmapOptions <highcharts_maps.options.plot_options.flowmap.FlowmapOptions>` object rather than in
the :class:`FlowmapSeries <highcharts_maps.options.series.flowmap.FlowmapSeries>` will apply a marker to the
end of *every* flowmap series in your visualization.
:rtype: :class:`FlowmapMarker <highcharts_maps.utility_classes.markers.FlowmapMarker>` or
:obj:`None <python:None>
"""
return self._marker_end
@marker_end.setter
@class_sensitive(FlowmapMarker)
def marker_end(self, value):
self._marker_end = value
@property
def opacity(self) -> Optional[int | float | Decimal]:
"""Opacity for the link. Defaults to :obj:`None <python:None>`.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._opacity
@opacity.setter
def opacity(self, value):
self._opacity = validators.numeric(value,
allow_empty = True,
minimum = 0)
@property
def weight(self) -> Optional[int | float | Decimal]:
"""The weight for the link, which determines its thickness compared to other links. Defaults to
:obj:`None <python:None>`.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._weight
@weight.setter
def weight(self, value):
self._weight = validators.numeric(value, allow_empty = True)
[docs] @classmethod
def from_array(cls, value):
"""Generator method which produces a collection of :class:`FlowmapData`
instances derived from ``value``. Generally consumed by the setter methods in
series-type specific data classes.
:rtype: :class:`list <python:list>` of :obj:`FlowmapData` instances
"""
if not value:
return []
elif checkers.is_string(value):
try:
value = validators.json(value)
except (ValueError, TypeError):
pass
elif not checkers.is_iterable(value):
value = [value]
collection = []
for item in value:
if checkers.is_type(item, 'FlowmapData'):
as_obj = item
elif checkers.is_dict(item):
as_obj = cls.from_dict(item)
elif item is None or isinstance(item, constants.EnforcedNullType):
as_obj = cls()
if checkers.is_iterable(item, forbid_literals = (str, bytes, dict, UserDict)):
if len(item) == 3:
as_obj = cls(from_ = item[0],
to = item[1],
weight = item[2])
else:
raise errors.HighchartsValueError(f'FlowmapData expects an array of FlowmapData objects, '
f'or an iterable coercable to a FlowmapData object. '
f'However the iterable received could not be coerced.'
f'Expected a 3-member iterable, but the received iterable '
f'had {len(item)} members.')
else:
raise errors.HighchartsValueError(f'each data point supplied must either '
f'be a Connection Data Point or be '
f'coercable to one. Could not coerce: '
f'{item}')
collection.append(as_obj)
return collection
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
"""Convenience method which returns the keyword arguments used to initialize the
class from a Highcharts Javascript-compatible :class:`dict <python:dict>` object.
:param as_dict: The HighCharts JS compatible :class:`dict <python:dict>`
representation of the object.
:type as_dict: :class:`dict <python:dict>`
:returns: The keyword arguments that would be used to initialize an instance.
:rtype: :class:`dict <python:dict>`
"""
kwargs = {
'accessibility': as_dict.get('accessibility', None),
'class_name': as_dict.get('className', None),
'color': as_dict.get('color', None),
'color_index': as_dict.get('colorIndex', None),
'custom': as_dict.get('custom', None),
'description': as_dict.get('description', None),
'events': as_dict.get('events', None),
'id': as_dict.get('id', None),
'label_rank': as_dict.get('labelrank',
None) or as_dict.get('labelRank',
None),
'name': as_dict.get('name', None),
'selected': as_dict.get('selected', None),
'data_labels': as_dict.get('dataLabels', None),
'drag_drop': as_dict.get('dragDrop', None),
'from_': as_dict.get('from', None),
'to': as_dict.get('to', None),
'weight': as_dict.get('weight', None),
'curve_factor': as_dict.get('curveFactor', None),
'fill_color': as_dict.get('fillColor', None),
'fill_opacity': as_dict.get('fillOpacity', None),
'grow_towards': as_dict.get('growTowards', None),
'line_width': as_dict.get('lineWidth', None),
'marker_end': as_dict.get('markerEnd', None),
'opacity': as_dict.get('opacity', None),
}
return kwargs
def _to_untrimmed_dict(self, in_cls = None) -> dict:
untrimmed = {
'weight': self.weight,
'dataLabels': self.data_labels,
'dragDrop': self.drag_drop,
'from': self.from_,
'to': self.to,
'accessibility': self.accessibility,
'className': self.class_name,
'color': self.color,
'colorIndex': self.color_index,
'custom': self.custom,
'description': self.description,
'events': self.events,
'id': self.id,
'labelrank': self.label_rank,
'name': self.name,
'selected': self.selected,
'curve_factor': self.curve_factor,
'fill_color': self.fill_color,
'fill_opacity': self.fill_opacity,
'grow_towards': self.grow_towards,
'line_width': self.line_width,
'marker_end': self.marker_end,
'opacity': self.opacity,
}
return untrimmed