Skip to content

Commit

Permalink
feat!: Add label wrapping (#80)
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 authored Mar 27, 2024
1 parent 844f171 commit c683b03
Show file tree
Hide file tree
Showing 20 changed files with 547 additions and 115 deletions.
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ repos:
- id: fix-byte-order-marker
- id: trailing-whitespace
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.1
rev: 24.3.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.6.1
rev: v1.9.0
hooks:
- id: mypy
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4
rev: v1.5.5
hooks:
- id: insert-license
name: Insert Licence for Python, YAML and Dockerfiles
Expand Down Expand Up @@ -67,6 +67,6 @@ repos:
- --comment-style
- "/*| *| */"
- repo: https://github.com/fsfe/reuse-tool
rev: v2.1.0
rev: v3.0.1
hooks:
- id: reuse
5 changes: 5 additions & 0 deletions capellambse_context_diagrams/collectors/dataflow_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def only_involved(
functions: cabc.Iterable[fa.FunctionalExchange],
attributes: tuple[str, str],
) -> cabc.Iterable[fa.FunctionalExchange]:
"""Exchange filter function for collecting edges."""
src_attr, trg_attr = attributes
src_getter = operator.attrgetter(src_attr)
trg_getter = operator.attrgetter(trg_attr)
Expand All @@ -44,6 +45,7 @@ def collector(
cabc.Iterable[fa.FunctionalExchange],
] = only_involved,
) -> _elkjs.ELKInputData:
"""Main collector that calls either default or portless collectors."""
return COLLECTORS[diagram.type](diagram, params, exchange_filter)


Expand All @@ -60,6 +62,7 @@ def collector_portless(
],
attribute: str = "involved_activities",
) -> _elkjs.ELKInputData:
"""Collector function for the operational layer."""
data = makers.make_diagram(diagram)
activities = getattr(diagram.target, attribute)
filter = functools.partial(
Expand Down Expand Up @@ -110,6 +113,7 @@ def collector_default(
],
attribute: str = "involved_functions",
) -> _elkjs.ELKInputData:
"""Collector for all other layers than operational architecture."""
data = makers.make_diagram(diagram)
functions = getattr(diagram.target, attribute)
filter = functools.partial(
Expand Down Expand Up @@ -153,3 +157,4 @@ def collector_default(
modeltypes.DiagramType.OAIB: collector_portless,
modeltypes.DiagramType.SDFB: collector_default,
}
"""Collector registry."""
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
49 changes: 23 additions & 26 deletions capellambse_context_diagrams/collectors/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import logging
import typing as t

from capellambse import helpers
from capellambse.model import common
from capellambse.model.crosslayer import interaction
from capellambse.model.modeltypes import DiagramType as DT
Expand Down Expand Up @@ -126,29 +125,21 @@ def exchange_data_collector(
if data.is_hierarchical:
target, source = source, target

label = collect_label(data.exchange)
for filter in data.filter_iterable:
try:
label = filters.FILTER_LABEL_ADJUSTERS[filter](
data.exchange, label
)
except KeyError:
logger.exception(
"There is no filter labelled: '%s' in filters.FILTER_LABEL_ADJUSTERS",
filter,
)

params = (data.params or {}).copy()
# Remove simple render parameters from params
no_edgelabels: bool = params.pop("no_edgelabels", False)
params.pop("transparent_background", False)
_ = params.pop("font_family", "Open Sans")
_ = params.pop("font_size", 12)

render_adj: dict[str, t.Any] = {}
for name, value in params.items():
try:
filters.RENDER_ADJUSTERS[name](value, data.exchange, render_adj)
except KeyError:
logger.exception(
"There is no render parameter solver labelled: '%s' in filters.RENDER_ADJUSTERS",
"There is no render parameter solver labelled: '%s' "
"in filters.RENDER_ADJUSTERS",
name,
)

Expand All @@ -159,19 +150,25 @@ def exchange_data_collector(
"targets": [render_adj.get("targets", target.uuid)],
},
)

label = collect_label(data.exchange)
for filter in data.filter_iterable:
try:
label = filters.FILTER_LABEL_ADJUSTERS[filter](
data.exchange, label
)
except KeyError:
logger.exception(
"There is no filter labelled: '%s' in "
"filters.FILTER_LABEL_ADJUSTERS",
filter,
)

if label and not no_edgelabels:
width, height = helpers.extent_func(label)
data.elkdata["edges"][-1]["labels"] = [
{
"text": render_adj.get("labels_text", label),
"width": render_adj.get(
"labels_width", width + 2 * makers.LABEL_HPAD
),
"height": render_adj.get(
"labels_height", height + 2 * makers.LABEL_VPAD
),
}
]
data.elkdata["edges"][-1]["labels"] = makers.make_label(
render_adj.get("labels_text", label),
max_width=makers.MAX_LABEL_WIDTH,
)

return source, target

Expand Down
85 changes: 64 additions & 21 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,18 @@
"""Default size of ports in pixels."""
PORT_PADDING = 2
"""Default padding of ports in pixels."""
LABEL_HPAD = 15
LABEL_HPAD = 3
"""Horizontal padding left and right of the label."""
LABEL_VPAD = 1
"""Vertical padding above and below the label."""
MAX_LABEL_WIDTH = 200
"""Maximum width for edge labels."""
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 +59,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 +81,37 @@ 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)
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,
}
label_width, label_height = chelpers.get_text_extent(text)
icon_width, _ = icon
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": label_height + 2 * LABEL_VPAD,
"layoutOptions": layout_options,
}
)
return labels


class _LabelBuilder(te.TypedDict, total=True):
Expand All @@ -99,26 +128,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 All @@ -136,7 +179,7 @@ def calculate_height_and_width(
"""Calculate the size (width and height) from given labels for a box."""
icon = icon_size + icon_padding * 2
_height = sum(label["height"] + 2 * LABEL_VPAD for label in labels) + icon
min_width = max(label["width"] + 2 * LABEL_HPAD for label in labels) + icon
min_width = max(label["width"] + 2 * LABEL_HPAD for label in labels)
width = min_width if slim_width else max(width, min_width)
return width, max(height, _height)

Expand Down
Loading

0 comments on commit c683b03

Please sign in to comment.