Skip to content

Commit

Permalink
feat!: Add automatic label wrapping
Browse files Browse the repository at this point in the history
This prevents way too long labels and therefore very wide boxes. Set per
default.
  • Loading branch information
ewuerger committed Mar 18, 2024
1 parent 137fc34 commit 3960e31
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 62 deletions.
10 changes: 10 additions & 0 deletions capellambse_context_diagrams/collectors/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def collector(
box = makers.make_box(
diagram.target.parent,
no_symbol=diagram.display_symbols_as_boxes,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
box["children"] = [centerbox]
del data["children"][0]
Expand Down Expand Up @@ -96,6 +97,11 @@ def collector(
parent_box.setdefault("children", []).append(
global_boxes.pop(child.uuid)
)
for label in parent_box["labels"]:
label[
"layoutOptions"
] = makers.CENTRIC_LABEL_LAYOUT_OPTIONS

_move_edge_to_local_edges(
parent_box, connections, local_ports, diagram, data
)
Expand All @@ -107,6 +113,10 @@ def collector(
if child_boxes:
centerbox["children"] = child_boxes
centerbox["width"] = makers.EOI_WIDTH
for label in centerbox.get("labels", []):
label.setdefault("layoutOptions", {}).update(
makers.DEFAULT_LABEL_LAYOUT_OPTIONS
)

centerbox["height"] = max(centerbox["height"], *stack_heights.values())
return data
Expand Down
32 changes: 24 additions & 8 deletions capellambse_context_diagrams/collectors/exchanges.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ class ExchangeCollector(metaclass=abc.ABCMeta):

def __init__(
self,
diagram: context.InterfaceContextDiagram
| context.FunctionalContextDiagram,
diagram: (
context.InterfaceContextDiagram | context.FunctionalContextDiagram
),
data: _elkjs.ELKInputData,
params: dict[str, t.Any],
) -> None:
Expand Down Expand Up @@ -130,8 +131,9 @@ def collect(self) -> None:


def get_elkdata_for_exchanges(
diagram: context.InterfaceContextDiagram
| context.FunctionalContextDiagram,
diagram: (
context.InterfaceContextDiagram | context.FunctionalContextDiagram
),
collector_type: type[ExchangeCollector],
params: dict[str, t.Any],
) -> _elkjs.ELKInputData:
Expand Down Expand Up @@ -180,12 +182,20 @@ def make_boxes(
comp: common.GenericElement, functions: list[common.GenericElement]
) -> None:
if comp.uuid not in made_children:
box = makers.make_box(comp, no_symbol=True)
box["children"] = [
children = [
makers.make_box(c)
for c in functions
if c in self.get_alloc_functions(comp)
]
if children:
layout_options = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
else:
layout_options = makers.CENTRIC_LABEL_LAYOUT_OPTIONS

box = makers.make_box(
comp, no_symbol=True, layout_options=layout_options
)
box["children"] = children
self.data["children"].append(box)
made_children.add(comp.uuid)

Expand Down Expand Up @@ -277,8 +287,14 @@ def collect(self) -> None:
self.obj, interface
)
if comp.uuid not in made_children:
box = makers.make_box(comp)
box["children"] = [makers.make_box(c) for c in functions]
children = [makers.make_box(c) for c in functions]
if children:
layout_options = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
else:
layout_options = makers.CENTRIC_LABEL_LAYOUT_OPTIONS

box = makers.make_box(comp, layout_options=layout_options)
box["children"] = children
self.data["children"].append(box)
made_children.add(comp.uuid)

Expand Down
80 changes: 61 additions & 19 deletions capellambse_context_diagrams/collectors/makers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
import collections.abc as cabc

import typing_extensions as te
from capellambse import helpers
from capellambse import helpers as chelpers
from capellambse.model import common, layers
from capellambse.svg import helpers as svghelpers
from capellambse.svg.decorations import icon_padding, icon_size

from .. import _elkjs, context
Expand All @@ -16,14 +17,16 @@
"""Default size of ports in pixels."""
PORT_PADDING = 2
"""Default padding of ports in pixels."""
LABEL_HPAD = 15
LABEL_HPAD = 5
"""Horizontal padding left and right of the label."""
LABEL_VPAD = 1
"""Vertical padding above and below the label."""
NEIGHBOR_VMARGIN = 20
"""Vertical space between two neighboring boxes."""
EOI_WIDTH = 150
EOI_WIDTH = 100
"""The width of the element of interest."""
MAX_BOX_WIDTH = 150
"""Maximum width of boxes."""
MIN_SYMBOL_WIDTH = 30
"""Minimum width of symbols."""
MIN_SYMBOL_HEIGHT = 17
Expand Down Expand Up @@ -54,6 +57,14 @@
"nodeLabels.placement": "INSIDE, V_TOP, H_CENTER"
}
"""Default layout options for a label."""
CENTRIC_LABEL_LAYOUT_OPTIONS: _elkjs.LayoutOptions = {
"nodeLabels.placement": "INSIDE, V_CENTER, H_CENTER"
}
"""Layout options for a centric label."""
SYMBOL_LAYOUT_OPTIONS: _elkjs.LayoutOptions = {
"nodeLabels.placement": "OUTSIDE, V_BOTTOM, H_CENTER"
}
"""Layout options for a symbol label."""


def make_diagram(diagram: context.ContextDiagram) -> _elkjs.ELKInputData:
Expand All @@ -68,21 +79,38 @@ def make_diagram(diagram: context.ContextDiagram) -> _elkjs.ELKInputData:

def make_label(
text: str,
icon: tuple[int | float, int | float] = (0, 0),
icon: tuple[int | float, int | float] = (ICON_WIDTH, ICON_HEIGHT),
layout_options: _elkjs.LayoutOptions | None = None,
) -> _elkjs.ELKInputLabel:
max_width: int | float | None = None,
) -> list[_elkjs.ELKInputLabel]:
"""Return an
[`ELKInputLabel`][capellambse_context_diagrams._elkjs.ELKInputLabel].
"""
label_width, label_height = helpers.get_text_extent(text)
label_width, label_height = chelpers.get_text_extent(text)
icon_width, icon_height = icon
layout_options = layout_options or DEFAULT_LABEL_LAYOUT_OPTIONS
return {
"text": text,
"width": icon_width + label_width + 2 * LABEL_HPAD,
"height": icon_height + label_height + 2 * LABEL_VPAD,
"layoutOptions": layout_options,
}
lines = [text]
if max_width is not None and label_width > max_width:
lines, _, _ = svghelpers.check_for_horizontal_overflow(
text,
max_width,
icon_padding,
icon_width,
)

layout_options = layout_options or CENTRIC_LABEL_LAYOUT_OPTIONS
labels: list[_elkjs.ELKInputLabel] = []
for line in lines:
label_width, label_height = chelpers.get_text_extent(line)
labels.append(
{
"text": line,
"width": icon_width + label_width + 2 * LABEL_HPAD,
"height": icon_height + label_height + 2 * LABEL_VPAD,
"layoutOptions": layout_options,
}
)
icon_height *= 0
return labels


class _LabelBuilder(te.TypedDict, total=True):
Expand All @@ -99,26 +127,40 @@ def make_box(
width: int | float = 0,
height: int | float = 0,
no_symbol: bool = False,
slim_width: bool = False,
slim_width: bool = True,
label_getter: cabc.Callable[
[common.GenericElement], cabc.Iterable[_LabelBuilder]
] = lambda i: [
{"text": i.name, "icon": (0, 0), "layout_options": {}}
{
"text": i.name,
"icon": (ICON_WIDTH, 0),
"layout_options": {},
}
], # type: ignore
max_label_width: int | float = MAX_BOX_WIDTH,
layout_options: _elkjs.LayoutOptions | None = None,
) -> _elkjs.ELKInputChild:
"""Return an
[`ELKInputChild`][capellambse_context_diagrams._elkjs.ELKInputChild].
"""
labels = [make_label(**label) for label in label_getter(obj)]
layout_options = layout_options or CENTRIC_LABEL_LAYOUT_OPTIONS
labels: list[_elkjs.ELKInputLabel] = []
for label_builder in label_getter(obj):
if not label_builder.get("layout_options"):
label_builder.setdefault("layout_options", {}).update(
layout_options
)

labels.extend(make_label(**label_builder, max_width=max_label_width))

if not no_symbol and is_symbol(obj):
if height < MIN_SYMBOL_HEIGHT:
height = MIN_SYMBOL_HEIGHT
elif height > MAX_SYMBOL_HEIGHT:
height = MAX_SYMBOL_HEIGHT
width = height * SYMBOL_RATIO
labels[0]["layoutOptions"] = {
"nodeLabels.placement": "OUTSIDE, V_BOTTOM, H_CENTER"
}
for label in labels:
label.setdefault("layoutOptions", {}).update(SYMBOL_LAYOUT_OPTIONS)
else:
width, height = calculate_height_and_width(
labels, width=width, height=height, slim_width=slim_width
Expand Down
2 changes: 1 addition & 1 deletion capellambse_context_diagrams/collectors/portless.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def collector(
if not diagram.display_symbols_as_boxes and makers.is_symbol(
diagram.target
):
height = max(makers.MIN_SYMBOL_HEIGHT, var_height)
height = makers.MIN_SYMBOL_HEIGHT + var_height
else:
height = var_height

Expand Down
13 changes: 11 additions & 2 deletions capellambse_context_diagrams/collectors/realization_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def collector(
if not (elements := lay_to_els.get(layer)): # type: ignore[call-overload]
continue

labels = [makers.make_label(layer)]
labels = makers.make_label(layer)
width, height = makers.calculate_height_and_width(labels)
layer_box: _elkjs.ELKInputChild = {
"id": elements[0]["layer"].uuid,
Expand Down Expand Up @@ -79,14 +79,23 @@ def collector(
continue

if not (owner_box := children.get(owner.uuid)):
owner_box = makers.make_box(owner, no_symbol=True)
owner_box = makers.make_box(
owner,
no_symbol=True,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
owner_box["height"] += element_box["height"]
children[owner.uuid] = owner_box
layer_box["children"].append(owner_box)

del layer_box["children"][index]
owner_box.setdefault("children", []).append(element_box)
owner_box["width"] += element_box["width"]
for label in owner_box["labels"]:
label["layoutOptions"].update(
makers.DEFAULT_LABEL_LAYOUT_OPTIONS
)

if (
source is not None
and source.owner.uuid in children
Expand Down
36 changes: 24 additions & 12 deletions capellambse_context_diagrams/collectors/tree_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __contains__(self, uuid: str) -> bool:
objects = self.data["children"] + self.data["edges"] # type: ignore[operator]
return uuid in {obj["id"] for obj in objects}

def process_class(self, cls, params):
def process_class(self, cls: ClassInfo, params: dict[str, t.Any]):
self._process_box(cls.source, cls.partition, params)

if not cls.primitive and isinstance(cls.target, information.Class):
Expand All @@ -58,15 +58,19 @@ def process_class(self, cls, params):
if (edge_id := edges[0].uuid) not in self.made_edges:
self.made_edges.add(edge_id)
text = cls.prop.name
start, end = cls.multiplicity
if cls.multiplicity is None:
start, end = "1", "1"
else:
start, end = cls.multiplicity

if start != "1" or end != "1":
text = f"[{start}..{end}] {text}"
self.data["edges"].append(
{
"id": edge_id,
"sources": [cls.source.uuid],
"targets": [cls.target.uuid],
"labels": [makers.make_label(text)],
"labels": makers.make_label(text),
}
)

Expand Down Expand Up @@ -95,7 +99,10 @@ def _make_box(
self, obj: information.Class, partition: int, params: dict[str, t.Any]
) -> _elkjs.ELKInputChild:
self.made_boxes.add(obj.uuid)
box = makers.make_box(obj)
box = makers.make_box(
obj,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
self._set_data_types_and_labels(box, obj)
_set_partitioning(box, partition, params)
self.data["children"].append(box)
Expand All @@ -122,6 +129,9 @@ def collector(
"""Return the class tree data for ELK."""
assert isinstance(diagram.target, information.Class)
data = generic.collector(diagram, no_symbol=True)
data["children"][0]["labels"][0].setdefault("layoutOptions", {}).update(
makers.DEFAULT_LABEL_LAYOUT_OPTIONS
)
all_associations: cabc.Iterable[
information.Association
] = diagram._model.search("Association")
Expand Down Expand Up @@ -351,8 +361,10 @@ def _get_all_non_edge_properties(
continue

text = _get_property_text(prop)
label = makers.make_label(text, layout_options=layout_options)
properties.append(label)
labels = makers.make_label(
text, icon=(makers.ICON_WIDTH, 0), layout_options=layout_options
)
properties.extend(labels)

if prop.type.uuid in data_types:
continue
Expand Down Expand Up @@ -386,7 +398,11 @@ def _get_property_text(prop: information.Property) -> str:
def _get_legend_labels(
obj: information.datatype.Enumeration | information.Class,
) -> cabc.Iterator[makers._LabelBuilder]:
yield {"text": obj.name, "icon": (0, 0), "layout_options": {}}
yield {
"text": obj.name,
"icon": (0, 0),
"layout_options": makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
}
if isinstance(obj, information.datatype.Enumeration):
labels = [literal.name for literal in obj.literals]
elif isinstance(obj, information.Class):
Expand All @@ -395,8 +411,4 @@ def _get_legend_labels(
return
layout_options = DATA_TYPE_LABEL_LAYOUT_OPTIONS
for label in labels:
yield {
"text": label,
"icon": (0, 0),
"layout_options": layout_options,
}
yield {"text": label, "icon": (0, 0), "layout_options": layout_options}
18 changes: 8 additions & 10 deletions capellambse_context_diagrams/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,16 @@ class type that stores all previously named classes.
assert parent is not None
if isinstance(parent, diagram.Box) and not parent.port:
if parent.JSON_TYPE != "symbol":
parent.labels.append(child["text"])
parent.styleoverrides |= self.get_styleoverrides(child)
else:
parent.labels.append(
diagram.Box(
ref
+ (child["position"]["x"], child["position"]["y"]),
(child["size"]["width"], child["size"]["height"]),
labels=[child["text"]],
styleoverrides=self.get_styleoverrides(child),
)

parent.labels.append(
diagram.Box(
ref + (child["position"]["x"], child["position"]["y"]),
(child["size"]["width"], child["size"]["height"]),
labels=[child["text"]],
styleoverrides=self.get_styleoverrides(child),
)
)
else:
assert isinstance(parent, diagram.Edge)
parent.labels.append(
Expand Down
Loading

0 comments on commit 3960e31

Please sign in to comment.