from typing import Optional, List
from decimal import Decimal
from validator_collection import validators
from highcharts_core import errors
from highcharts_core.decorators import class_sensitive, validate_types
from highcharts_core.metaclasses import HighchartsMeta
from highcharts_core.options.plot_options.generic import GenericTypeOptions
from highcharts_core.options.plot_options.link import LinkOptions
from highcharts_core.utility_classes.zones import Zone
from highcharts_core.utility_classes.shadows import ShadowOptions
from highcharts_core.utility_classes.javascript_functions import CallbackFunction
from highcharts_core.utility_classes.events import SimulationEvents
[docs]class LayoutAlgorithm(HighchartsMeta):
"""Configuration of how to lay out the Network Graph."""
def __init__(self, **kwargs):
self._approximation = None
self._attractive_force = None
self._enable_simulation = None
self._friction = None
self._gravitational_constant = None
self._initial_position_radius = None
self._initial_positions = None
self._integration = None
self._link_length = None
self._max_iterations = None
self._max_speed = None
self._repulsive_force = None
self._theta = None
self._type = None
self.approximation = kwargs.get('approximation', None)
self.attractive_force = kwargs.get('attractive_force', None)
self.enable_simulation = kwargs.get('enable_simulation', None)
self.friction = kwargs.get('friction', None)
self.gravitational_constant = kwargs.get('gravitational_constant', None)
self.initial_position_radius = kwargs.get('initial_position_radius', None)
self.initial_positions = kwargs.get('initial_positions', None)
self.integration = kwargs.get('integration', None)
self.link_length = kwargs.get('link_length', None)
self.max_iterations = kwargs.get('max_iterations', None)
self.max_speed = kwargs.get('max_speed', None)
self.repulsive_force = kwargs.get('repulsive_force', None)
self.theta = kwargs.get('theta', None)
self.type = kwargs.get('type', None)
@property
def approximation(self) -> Optional[str]:
"""Approximation used to calculate repulsive forces affecting nodes.
When :obj:`None <python:None>`, when calculateing net force, nodes are compared
against each other, which gives ``O(N^2)`` complexity. Using ``barnes-hut``
approximation, we decrease this to ``O(N log N)``, but the resulting graph will
have a different layout.
.. note::
Barnes-Hut approximation divides space into rectangles via quad tree, where
forces exerted on nodes are calculated directly for nearby cells, and for all
others, cells are treated as a separate node with center of mass.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._approximation
@approximation.setter
def approximation(self, value):
self._approximation = validators.string(value, allow_empty = True)
@property
def attractive_force(self) -> Optional[CallbackFunction]:
"""JavaScript function which calculates the attraction force applied on a node
which is conected to another node by a link.
The (JavaScript) function should be passed two arguments:
* ``d`` - which is the current distance between two nodes
* ``k`` - which is the desired distance between two nodes
If :obj:`None <python:None>`, defaults to:
.. code-block:: javascript
function (d, k) {
return k * k / d;
}
If :meth:`LayoutAlgorithm.integration` is ``'verlet'``, then if
:obj:`None <python:None>` defaults to:
.. code-block:: javascript
function (d, k) {
return (k - d) / d;
}
:rtype: :class:`CallbackFunction` or :obj:`None <python:None>`
"""
return self._attractive_force
@attractive_force.setter
@class_sensitive(CallbackFunction)
def attractive_force(self, value):
self._attractive_force = value
@property
def enable_simulation(self) -> Optional[bool]:
"""If ``True``, enables live simulation of the algorithm's implementation. All
nodes are animated as the force applies to them. Defaults to ``False``.
.. warning::
EXPERIMENTAL!
:rtype: :class:`bool <python:bool>`
"""
return self._enable_simulation
@enable_simulation.setter
def enable_simulation(self, value):
if value is None:
self._enable_simulation = None
else:
self._enable_simulation = bool(value)
@property
def friction(self) -> Optional[int | float | Decimal]:
"""Friction applied on forces to prevent nodes rushing to fast to the desired
positions. Defaults to ``-0.981``.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._friction
@friction.setter
def friction(self, value):
self._friction = validators.numeric(value, allow_empty = True)
@property
def gravitational_constant(self) -> Optional[int | float | Decimal]:
"""Gravitational const used in the barycenter force of the algorithm. Defaults to
``0.0625``.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._gravitational_constant
@gravitational_constant.setter
def gravitational_constant(self, value):
self._gravitational_constant = validators.numeric(value, allow_empty = True)
@property
def initial_position_radius(self) -> Optional[int | float | Decimal]:
"""When :meth:`LayoutAlgorithm.initial_positions` is set to ``'circle'``, this
setting is the distance from the center of the circle at which nodes will be
created. Defaults to ``1``.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._initial_position_radius
@initial_position_radius.setter
def initial_position_radius(self, value):
self._initial_position_radius = validators.numeric(value, allow_empty = True)
@property
def initial_positions(self) -> Optional[str]:
"""Initial layout algorithm for positioning nodes. Defaults to ``'circle'``.
Accepts the following options:
* ``"circle"``
* ``"random"``
* a JavaScript function where positions should be set on each node
(``this.nodes``) as ``node.plotX`` and ``node.plotY``
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._initial_positions
@initial_positions.setter
def initial_positions(self, value):
self._initial_positions = validators.string(value, allow_empty = True)
@property
def integration(self) -> Optional[str]:
"""Integration type. Defaults to ``'euler'``.
Available options are:
* ``'euler'``
* ``'verlet'``
Integration determines how forces are applied on particles. In Euler integration,
force is applied directly as ``newPosition += velocity``;. In Verlet integration,
new position is based on the previous posittion without velocity:
``newPosition += previousPosition - newPosition``.
Note that different integrations give different results as forces are different.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._integration
@integration.setter
def integration(self, value):
if not value:
self._integration = None
else:
value = validators.string(value)
value = value.lower()
if value not in ['euler', 'verlet']:
raise errors.HighchartsValueError(f'integration expects either "euler" '
f'or "verlet". Was: {value}')
self._integration = value
@property
def link_length(self) -> Optional[int | float | Decimal]:
"""Ideal length (px) of the link between two nodes. When
:obj:`None <python:None>`, length is calculated (in JavaScript) as:
``Math.pow(availableWidth * availableHeight / nodesLength, 0.4);``
.. note::
Because of the algorithm specification, length of each link might be not exactly
as specified.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._link_length
@link_length.setter
def link_length(self, value):
self._link_length = validators.numeric(value,
allow_empty = True,
minimum = 0)
@property
def max_iterations(self) -> Optional[int]:
"""Maximum number of iterations before algorithm will stop. In general, the
algorithm should find positions sooner, but when rendering huge number of nodes,
it is recommended to increase this value as finding perfect graph positions can
require more time.
Defaults to ``1000``.
:rtype: :class:`int <python:int>` or :obj:`None <python:None>`
"""
return self._max_iterations
@max_iterations.setter
def max_iterations(self, value):
self._max_iterations = validators.integer(value,
allow_empty = True,
minimum = 1)
@property
def max_speed(self) -> Optional[int | float | Decimal]:
"""Maximum speed that a node can attain in one iteration. Defaults to ``10``.
In terms of simulation, it's a maximum translation (in pixels) that a node can
move (in both x and y dimensions). While friction is applied on all nodes,
``max_speed`` is applied only for nodes that move very fast, for example, small or
disconnected ones.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._max_speed
@max_speed.setter
def max_speed(self, value):
self._max_speed = validators.numeric(value,
allow_empty = True,
minimum = 0)
@property
def repulsive_force(self) -> Optional[CallbackFunction]:
"""JavaScript function which calculates the repulsive force applied on a node
which is conected to another node by a link.
The (JavaScript) function should be passed two arguments:
* ``d`` - which is the current distance between two nodes
* ``k`` - which is the desired distance between two nodes
If :obj:`None <python:None>`, defaults to:
.. code-block:: javascript
function (d, k) {
return k * k / d;
}
If :meth:`LayoutAlgorithm.integration` is ``'verlet'``, then if
:obj:`None <python:None>` defaults to:
.. code-block:: javascript
function (d, k) {
return (k - d) / d * (k > d ? 1 : 0)
}
:rtype: :class:`CallbackFunction` or :obj:`None <python:None>`
"""
return self._repulsive_force
@repulsive_force.setter
@class_sensitive(CallbackFunction)
def repulsive_force(self, value):
self._repulsive_force = value
@property
def theta(self) -> Optional[int | float | Decimal]:
"""Deteremines when distance between cell and node is small enough to caculate
forces. Defaults to ``0.5``.
The value of theta is compared directly with quotient ``s / d``, where ``s`` is
the size of the cell, and ``d`` is the distance between the center of the cell's
mass and the currently compared node.
.. warning::
Applies only to the ``barnes-hut`` :meth:`LayoutAlgorithm.approximation`.
:rtype: numeric or :obj:`None <python:None>`
"""
return self._theta
@theta.setter
def theta(self, value):
self._theta = validators.numeric(value, allow_empty = True)
@property
def type(self) -> Optional[str]:
"""Type of algorithm used when positioning nodes. Defaults to
``'reingold-fruchterman'``.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._type
@type.setter
def type(self, value):
self._type = validators.string(value, allow_empty = True)
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
'approximation': as_dict.get('approximation', None),
'attractive_force': as_dict.get('attractiveForce', None),
'enable_simulation': as_dict.get('enableSimulation', None),
'friction': as_dict.get('friction', None),
'gravitational_constant': as_dict.get('gravitationalConstant', None),
'initial_position_radius': as_dict.get('initialPositionRadius', None),
'initial_positions': as_dict.get('initialPositions', None),
'integration': as_dict.get('integration', None),
'link_length': as_dict.get('linkLength', None),
'max_iterations': as_dict.get('maxIterations', None),
'max_speed': as_dict.get('maxSpeed', None),
'repulsive_force': as_dict.get('repulsiveForce', None),
'theta': as_dict.get('theta', None),
'type': as_dict.get('type', None)
}
return kwargs
def _to_untrimmed_dict(self, in_cls = None) -> dict:
untrimmed = {
'approximation': self.approximation,
'attractiveForce': self.attractive_force,
'enableSimulation': self.enable_simulation,
'friction': self.friction,
'gravitationalConstant': self.gravitational_constant,
'initialPositionRadius': self.initial_position_radius,
'initialPositions': self.initial_positions,
'integration': self.integration,
'linkLength': self.link_length,
'maxIterations': self.max_iterations,
'maxSpeed': self.max_speed,
'repulsiveForce': self.repulsive_force,
'theta': self.theta,
'type': self.type
}
return untrimmed
class NetworkGraphOptions(GenericTypeOptions):
"""General options to apply to all Network Graph series types.
A network graph is a type of relationship chart, where connnections (links)
attract nodes (points) and other nodes repulse each other.
.. figure:: ../../../_static/networkgraph-example.png
:alt: NetworkGraph Example Chart
:align: center
"""
def __init__(self, **kwargs):
self._color_index = None
self._crisp = None
self._draggable = None
self._find_nearest_point_by = None
self._layout_algorithm = None
self._line_width = None
self._link = None
self._relative_x_value = None
self._shadow = None
self._zones = None
self.color_index = kwargs.get('color_index', None)
self.crisp = kwargs.get('crisp', None)
self.draggable = kwargs.get('draggable', None)
self.find_nearest_point_by = kwargs.get('find_nearest_point_by', None)
self.layout_algorithm = kwargs.get('layout_algorithm', None)
self.line_width = kwargs.get('line_width', None)
self.link = kwargs.get('link', None)
self.relative_x_value = kwargs.get('relative_x_value', None)
self.shadow = kwargs.get('shadow', None)
self.zones = kwargs.get('zones', None)
super().__init__(**kwargs)
@property
def color_index(self) -> Optional[int]:
"""When operating in :term:`styled mode`, a specific color index to use for the
series, so that its graphic representations are given the class name
``highcharts-color-{n}``.
Defaults to :obj:`None <python:None>`.
:rtype: :class:`int <python:int>` or :obj:`None <python:None>`
"""
return self._color_index
@color_index.setter
def color_index(self, value):
self._color_index = validators.integer(value,
allow_empty = True,
minimum = 0)
@property
def crisp(self) -> Optional[bool]:
"""If ``True``, each point or column edge is rounded to its nearest pixel in order
to render sharp on screen. Defaults to ``True``.
.. hint::
In some cases, when there are a lot of densely packed columns, this leads to
visible difference in column widths or distance between columns. In these cases,
setting ``crisp`` to ``False`` may look better, even though each column is
rendered blurry.
:rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
"""
return self._crisp
@crisp.setter
def crisp(self, value):
if value is None:
self._crisp = None
else:
self._crisp = bool(value)
@property
def draggable(self) -> Optional[bool]:
"""If ``True``, indicates that the nodes are draggable. Defaults to ``True``.
:rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
"""
return self._draggable
@draggable.setter
def draggable(self, value):
if value is None:
self._draggable = None
else:
self._draggable = bool(value)
@property
def events(self) -> Optional[SimulationEvents]:
"""Event handlers for a network graph series.
.. note::
These event hooks can also be attached to the series at run time using the
(JavaScript) ``Highcharts.addEvent()`` function.
:rtype: :class:`SimulationEvents <highcharts_core.utility_classes.events.SimulationEvents>` or
:obj:`None <python:None>`
"""
return self._events
@events.setter
@class_sensitive(SimulationEvents)
def events(self, value):
self._events = value
@property
def find_nearest_point_by(self) -> Optional[str]:
"""Determines whether the series should look for the nearest point in both
dimensions or just the x-dimension when hovering the series.
If :obj:`None <python:None>`, defaults to ``'xy'`` for scatter series and ``'x'``
for most other series. If the data has duplicate x-values, it is recommended to
set this to ``'xy'`` to allow hovering over all points.
Applies only to series types using nearest neighbor search (not direct hover) for
tooltip.
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
"""
return self._find_nearest_point_by
@find_nearest_point_by.setter
def find_nearest_point_by(self, value):
self._find_nearest_point_by = validators.string(value, allow_empty = True)
@property
def layout_algorithm(self) -> Optional[LayoutAlgorithm]:
"""Configuration of how to lay out the Network Graph.
:rtype: :class:`LayoutAlgorithm` or :obj:`None <python:None>`
"""
return self._layout_algorithm
@layout_algorithm.setter
@class_sensitive(LayoutAlgorithm)
def layout_algorithm(self, value):
self._layout_algorithm = value
@property
def line_width(self) -> Optional[int | float | Decimal]:
"""Pixel width of the graph line. Defaults to ``2``.
: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 link(self) -> Optional[LinkOptions]:
"""Link style options.
:rtype: :class:`LinkOptions` or :obj:`None <python:None>`
"""
return self._link
@link.setter
@class_sensitive(LinkOptions)
def link(self, value):
self._link = value
@property
def relative_x_value(self) -> Optional[bool]:
"""When ``True``, X values in the data set are relative to the current
:meth:`point_start <AreaOptions.point_start>`,
:meth:`point_interval <AreaOptions.point_interval>`, and
:meth:`point_interval_unit <AreaOptions.point_interval_unit>` settings. This
allows compression of the data for datasets with irregular X values. Defaults to
``False``.
The real X values are computed on the formula ``f(x) = ax + b``, where ``a`` is
the :meth:`point_interval <AreaOptions.point_interval>` (optionally with a time
unit given by :meth:`point_interval_unit <AreaOptions.point_interval_unit>`), and
``b`` is the :meth:`point_start <AreaOptions.point_start>`.
:rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
"""
return self._relative_x_value
@relative_x_value.setter
def relative_x_value(self, value):
if value is None:
self._relative_x_value = None
else:
self._relative_x_value = bool(value)
@property
def shadow(self) -> Optional[bool | ShadowOptions]:
"""Configuration for the shadow to apply to the tooltip. Defaults to
``False``.
If ``False``, no shadow is applied.
:returns: The shadow configuration to apply or a boolean setting which hides the
shadow or displays the default shadow.
:rtype: :class:`bool <python:bool>` or :class:`ShadowOptions`
"""
return self._shadow
@shadow.setter
def shadow(self, value):
if isinstance(value, bool):
self._shadow = value
elif not value:
self._shadow = None
else:
value = validate_types(value,
types = ShadowOptions)
self._shadow = value
@property
def zones(self) -> Optional[List[Zone]]:
"""An array defining zones within a series. Defaults to :obj:`None <python:None>`.
Zones can be applied to the X axis, Y axis or Z axis for bubbles, according to the
:meth:`zone_axis <AreaOptions.zone_axis>` setting.
.. warning::
The zone definitions have to be in ascending order regarding to the value.
:rtype: :obj:`None <python:None>` or :class:`list <python:list>` of
:class:`Zone` instances
"""
return self._zones
@zones.setter
@class_sensitive(Zone,
force_iterable = True)
def zones(self, value):
self._zones = value
@classmethod
def _get_kwargs_from_dict(cls, as_dict):
kwargs = {
'accessibility': as_dict.get('accessibility', None),
'allow_point_select': as_dict.get('allowPointSelect', None),
'animation': as_dict.get('animation', None),
'class_name': as_dict.get('className', None),
'clip': as_dict.get('clip', None),
'color': as_dict.get('color', None),
'cursor': as_dict.get('cursor', None),
'custom': as_dict.get('custom', None),
'dash_style': as_dict.get('dashStyle', None),
'data_labels': as_dict.get('dataLabels', None),
'description': as_dict.get('description', None),
'enable_mouse_tracking': as_dict.get('enableMouseTracking', None),
'events': as_dict.get('events', None),
'include_in_data_export': as_dict.get('includeInDataExport', None),
'keys': as_dict.get('keys', None),
'label': as_dict.get('label', None),
'legend_symbol': as_dict.get('legendSymbol', None),
'linked_to': as_dict.get('linkedTo', None),
'marker': as_dict.get('marker', None),
'on_point': as_dict.get('onPoint', None),
'opacity': as_dict.get('opacity', None),
'point': as_dict.get('point', None),
'point_description_formatter': as_dict.get('pointDescriptionFormatter', None),
'selected': as_dict.get('selected', None),
'show_checkbox': as_dict.get('showCheckbox', None),
'show_in_legend': as_dict.get('showInLegend', None),
'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None),
'sonification': as_dict.get('sonification', None),
'states': as_dict.get('states', None),
'sticky_tracking': as_dict.get('stickyTracking', None),
'threshold': as_dict.get('threshold', None),
'tooltip': as_dict.get('tooltip', None),
'turbo_threshold': as_dict.get('turboThreshold', None),
'visible': as_dict.get('visible', None),
'color_index': as_dict.get('colorIndex', None),
'crisp': as_dict.get('crisp', None),
'draggable': as_dict.get('draggable', None),
'find_nearest_point_by': as_dict.get('findNearestPointBy', None),
'layout_algorithm': as_dict.get('layoutAlgorithm', None),
'line_width': as_dict.get('lineWidth', None),
'link': as_dict.get('link', None),
'relative_x_value': as_dict.get('relativeXValue', None),
'shadow': as_dict.get('shadow', None),
'zones': as_dict.get('zones', None)
}
return kwargs
def _to_untrimmed_dict(self, in_cls = None) -> dict:
untrimmed = {
'colorIndex': self.color_index,
'crisp': self.crisp,
'draggable': self.draggable,
'findNearestPointBy': self.find_nearest_point_by,
'layoutAlgorithm': self.layout_algorithm,
'lineWidth': self.line_width,
'link': self.link,
'relativeXValue': self.relative_x_value,
'shadow': self.shadow,
'zones': self.zones
}
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