"""Configurable shapes for ``ipyforcegraph`` nodes."""
# Copyright (c) 2023 ipyforcegraph contributors.
# Distributed under the terms of the Modified BSD License.
from typing import Any, Optional, Sequence, Tuple, Union
import ipywidgets as W
import traitlets as T
from ..trait_utils import JSON_TYPES, coerce
from ._base import (
DEFAULT_RANK,
Behavior,
HasDimensions,
HasFillAndStroke,
HasOffsets,
ShapeBase,
TFeature,
TNumFeature,
_make_trait,
)
[docs]@W.register
class Text(HasFillAndStroke, HasOffsets):
"""Draw a text shape, with an optional background.
If the ``text`` trait is (or evaluates to) ``0`` or ``None``, no shape will be drawn.
"""
_model_name: str = T.Unicode("TextShapeModel").tag(sync=True)
text: TFeature = _make_trait("the text of a shape")
font: TFeature = _make_trait("the font face of a shape")
size: TNumFeature = _make_trait(
"the visible font size of text in ``px``", numeric=True
)
size_pixels: TNumFeature = _make_trait(
"the rendered size of text in ``px``. 3D only.", numeric=True
)
background: TFeature = _make_trait("the background fill color of a shape")
padding: TNumFeature = _make_trait(
"the padding around the shape in ``px``", numeric=True
)
def __init__(self, text: Optional[TFeature] = None, **kwargs: Any):
if text is not None:
kwargs["text"] = text
super().__init__(**kwargs)
@T.validate("size", "padding", "size_pixels")
def _validate_text_numerics(self, proposal: T.Bunch) -> Any:
return coerce(proposal, JSON_TYPES.number)
[docs]@W.register
class Ellipse(HasDimensions):
"""Draw an ellipse shape.
If the ``width`` trait is (or evaluates to) ``0`` or ``None``, no shape will be drawn.
"""
_model_name: str = T.Unicode("EllipseShapeModel").tag(sync=True)
[docs]@W.register
class Rectangle(HasDimensions):
"""Draw a rectangle shape.
If the ``width`` trait is (or evaluates to) ``0`` or ``None``, no shape will be drawn.
"""
_model_name: str = T.Unicode("RectangleShapeModel").tag(sync=True)
[docs]@W.register
class NodeShapes(Behavior):
"""Change the shape of nodes using declarative statements.
The ``color`` and ``size`` traits affect the default circle, and compose
with :class:`~ipyforcegraph.behaviors.selection.NodeSelection`.
If non-empty, custom ``shapes`` will override the simple ``size`` and
``color``, and will require custom handling with ``column_name`` to reflect
user selection.
"""
_model_name: str = T.Unicode("NodeShapeModel").tag(sync=True)
size: TFeature = _make_trait("the size of the default circle shape", numeric=True)
color: TFeature = _make_trait("the color of the default circle shape")
shapes: Tuple[ShapeBase] = W.TypedTuple(
T.Instance(ShapeBase),
help="the shapes to draw for each ``node``",
).tag(sync=True, **W.widget_serialization)
def __init__(self, *shapes: Union[Sequence[ShapeBase], ShapeBase], **kwargs: Any):
if len(shapes) == 1 and isinstance(shapes, list):
shapes = shapes[0]
kwargs["shapes"] = shapes
super().__init__(**kwargs)
@T.default("rank")
def _default_rank(self) -> Optional[int]:
return DEFAULT_RANK.shapes
@T.validate("size")
def _validate_node_shape_numerics(self, proposal: T.Bunch) -> Any:
return coerce(proposal, JSON_TYPES.number)
[docs]@W.register
class LinkShapes(Behavior):
"""
Customize the shape of the ``links``.
Custom ``shapes`` will be drawn on top of default lines, and may not
interact predictably with ``curvature``.
.. note::
``line_dash`` is not displayed in :class:`~ipyforcegraph.graphs.ForceGraph3D`.
"""
_model_name: str = T.Unicode("LinkShapeModel").tag(sync=True)
color: TFeature = _make_trait("the color of the link")
curvature: TNumFeature = _make_trait(
"the curvature of the link, 0: straight, 1: circular", numeric=True
)
line_dash: TFeature = _make_trait(
"the dash line pattern of the link, e.g., [2, 1] for ``-- -- --``",
stringy=False,
by_column=False,
)
width: TNumFeature = _make_trait("the width of the link", numeric=True)
shapes: Tuple[ShapeBase] = W.TypedTuple(
T.Instance(ShapeBase),
help="the shapes to draw for each ``link``",
).tag(sync=True, **W.widget_serialization)
def __init__(self, *shapes: Union[Sequence[ShapeBase], ShapeBase], **kwargs: Any):
if len(shapes) == 1 and isinstance(shapes, list):
shapes = shapes[0]
kwargs["shapes"] = shapes
super().__init__(**kwargs)
@T.default("rank")
def _default_rank(self) -> Optional[int]:
return DEFAULT_RANK.shapes
@T.validate("curvature", "width")
def _validate_link_shape_numerics(self, proposal: T.Bunch) -> Any:
return coerce(proposal, JSON_TYPES.number)
@T.validate("line_dash")
def _validate_link_shape_arrays(self, proposal: T.Bunch) -> Any:
return coerce(proposal, JSON_TYPES.array)
[docs]@W.register
class LinkArrows(Behavior):
"""Customize the size, position, and color of arrows on ``links``."""
_model_name: str = T.Unicode("LinkArrowModel").tag(sync=True)
color: TFeature = _make_trait("the color of the arrow")
length: TNumFeature = _make_trait("the length of the arrow", numeric=True)
relative_position: TNumFeature = _make_trait(
"the relative position of the arrow along the link, 0.0: ``source`` end, 1.0: ``target`` end",
numeric=True,
)
@T.validate("length", "relative_position")
def _validate_arrow_numerics(self, proposal: T.Bunch) -> Any:
return coerce(proposal, JSON_TYPES.number)