From 19fb64e8a4c11ceb342595c95626e06da0d607ba Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 16 Oct 2023 14:11:48 +0200 Subject: [PATCH 1/9] feat(class-tree): Add new diagram type --- capellambse_context_diagrams/__init__.py | 12 ++- .../collectors/class_tree.py | 78 +++++++++++++++++++ capellambse_context_diagrams/context.py | 49 +++++++++++- capellambse_context_diagrams/serializers.py | 5 +- 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 capellambse_context_diagrams/collectors/class_tree.py diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py index 4c7230dc..f0d52bd6 100644 --- a/capellambse_context_diagrams/__init__.py +++ b/capellambse_context_diagrams/__init__.py @@ -24,7 +24,7 @@ from capellambse.diagram import COLORS, CSSdef, capstyle from capellambse.model import common -from capellambse.model.crosslayer import fa +from capellambse.model.crosslayer import fa, information from capellambse.model.layers import ctx, la, oa, pa from capellambse.model.modeltypes import DiagramType @@ -47,6 +47,7 @@ def init() -> None: """Initialize the extension.""" register_classes() register_interface_context() + register_class_tree() # register_functional_context() XXX: Future @@ -148,3 +149,12 @@ def register_functional_context() -> None: attr_name, context.FunctionalContextAccessor(dgcls.value), ) + + +def register_class_tree() -> None: + """Add the `class_tree_diagram` attribute to `ModelObject`s.""" + common.set_accessor( + information.Class, + "class_tree_diagram", + context.ClassTreeAccessor(DiagramType.CDB.value), + ) diff --git a/capellambse_context_diagrams/collectors/class_tree.py b/capellambse_context_diagrams/collectors/class_tree.py new file mode 100644 index 00000000..177990de --- /dev/null +++ b/capellambse_context_diagrams/collectors/class_tree.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2022 Copyright DB Netz AG and the capellambse-context-diagrams contributors +# SPDX-License-Identifier: Apache-2.0 +"""This submodule defines the collector for the Class-Tree diagram.""" +from __future__ import annotations + +import collections.abc as cabc +import typing as t + +from capellambse import helpers +from capellambse.model.crosslayer import information + +from .. import _elkjs, context +from . import generic, makers + +LAYOUT_OPTIONS: _elkjs.LayoutOptions = { + "algorithm": "layered", + "edgeRouting": "ORTHOGONAL", + "elk.direction": "DOWN", + "partitioning.activate": True, + "edgeLabels.sideSelection": "ALWAYS_DOWN", +} + + +def collector( + diagram: context.ContextDiagram, params: dict[str, t.Any] | None = None +) -> _elkjs.ELKInputData: + """Return the class tree data for ELK.""" + assert isinstance(diagram.target, information.Class) + data = generic.collector(diagram, no_symbol=True) + data["children"][0]["layoutOptions"] = {} + data["children"][0]["layoutOptions"]["elk.partitioning.partition"] = 0 + data["layoutOptions"] = LAYOUT_OPTIONS + data["layoutOptions"]["algorithm"] = (params or {})["algorithm"] + data["layoutOptions"]["elk.direction"] = (params or {})["direction"] + data["layoutOptions"]["edgeRouting"] = (params or {})["edgeRouting"] + + made_boxes: set[str] = set() + for uid, (source, target) in get_all_classes(diagram.target): + property_uuid, text, partition = uid.split(" ")[1:4] + if target.uuid not in made_boxes: + made_boxes.add(target.uuid) + box = makers.make_box(target) + box["layoutOptions"] = {} + box["layoutOptions"]["elk.partitioning.partition"] = int(partition) + data["children"].append(box) + + width, height = helpers.extent_func(text) + label: _elkjs.ELKInputLabel = { + "text": text, + "width": width + 2 * makers.LABEL_HPAD, + "height": height + 2 * makers.LABEL_VPAD, + } + data["edges"].append( + { + "id": property_uuid, + "sources": [source.uuid], + "targets": [target.uuid], + "labels": [label], + } + ) + return data + + +def get_all_classes( + root: information.Class, partition: int = 0 +) -> cabc.Iterator[tuple[str, tuple[information.Class, information.Class]]]: + """Yield all classes of the class tree.""" + partition += 1 + classes: dict[str, tuple[information.Class, information.Class]] = {} + for prop in root.properties: + if prop.type.xtype.endswith("Class"): + edge_id = f"{root.name} {prop.uuid} {prop.name}" + edge_id = f"{edge_id} {partition} {prop.type.name}" + if edge_id not in classes: + classes[edge_id] = (root, prop.type) + classes.update(dict(get_all_classes(prop.type))) + + yield from classes.items() diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 08f0f409..64d1d2f4 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -15,7 +15,7 @@ from capellambse.model import common, diagram, modeltypes from . import _elkjs, filters, serializers, styling -from .collectors import exchanges, get_elkdata +from .collectors import class_tree, exchanges, get_elkdata logger = logging.getLogger(__name__) @@ -129,6 +129,26 @@ def __get__( # type: ignore ) +class ClassTreeAccessor(ContextAccessor): + """Provides access to the class tree diagrams.""" + + # pylint: disable=super-init-not-called + def __init__(self, diagclass: str) -> None: + self._dgcls = diagclass + + def __get__( # type: ignore + self, + obj: common.T | None, + objtype: type | None = None, + ) -> common.Accessor | ContextDiagram: + """Make a ContextDiagram for the given model object.""" + del objtype + if obj is None: # pragma: no cover + return self + assert isinstance(obj, common.GenericElement) + return self._get(obj, ClassTreeDiagram, "{}_class_tree") + + class ContextDiagram(diagram.AbstractDiagram): """An automatically generated context diagram. @@ -305,3 +325,30 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: self, exchanges.FunctionalContextCollector, params ) return super()._create_diagram(params) + + +class ClassTreeDiagram(ContextDiagram): + """An automatically generated ClassTree Diagram. + + This diagram is exclusively for ``Class``es. + """ + + def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None: + super().__init__(class_, obj, **kw, display_symbols_as_boxes=True) + + @property + def uuid(self) -> str: # type: ignore + """Returns the UUID of the diagram.""" + return f"{self.target.uuid}_class-tree" + + @property + def name(self) -> str: # type: ignore + """Returns the name of the diagram.""" + return f"Class Tree of {self.target.name}" + + def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: + params.setdefault("algorithm", "layered") + params.setdefault("direction", "DOWN") + params.setdefault("edgeRouting", "POLYLINE") + params["elkdata"] = class_tree.collector(self, params) + return super()._create_diagram(params) diff --git a/capellambse_context_diagrams/serializers.py b/capellambse_context_diagrams/serializers.py index 36b1cbd8..1f5d0b88 100644 --- a/capellambse_context_diagrams/serializers.py +++ b/capellambse_context_diagrams/serializers.py @@ -28,6 +28,8 @@ * `junction`. """ +REMAP_STYLECLASS: dict[str, str] = {"Property": "Association"} + class DiagramSerializer: """Serialize an ``elk_diagram`` into an @@ -47,7 +49,7 @@ class DiagramSerializer: def __init__(self, elk_diagram: context.ContextDiagram) -> None: self.model = elk_diagram.target._model self._diagram = elk_diagram - self._cache: dict[str, diagram.Box] = {} + self._cache: dict[str, diagram.Box | diagram.Edge] = {} def make_diagram(self, data: _elkjs.ELKOutputData) -> diagram.Diagram: """Transform a layouted diagram into an `diagram.Diagram`. @@ -136,6 +138,7 @@ class type that stores all previously named classes. self.diagram.add_element(element) self._cache[child["id"]] = element elif child["type"] == "edge": + styleclass = REMAP_STYLECLASS.get(styleclass, styleclass) # type: ignore[arg-type] element = diagram.Edge( [ ref + (point["x"], point["y"]) From 0c432b0d5cb6c3a95797f56fb7625deef77db4a1 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 16 Oct 2023 15:04:52 +0200 Subject: [PATCH 2/9] fix(class-tree): Fix styleclass for edges --- .../collectors/class_tree.py | 26 +++++++++++-------- capellambse_context_diagrams/serializers.py | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/capellambse_context_diagrams/collectors/class_tree.py b/capellambse_context_diagrams/collectors/class_tree.py index 177990de..eab5e98e 100644 --- a/capellambse_context_diagrams/collectors/class_tree.py +++ b/capellambse_context_diagrams/collectors/class_tree.py @@ -35,8 +35,8 @@ def collector( data["layoutOptions"]["edgeRouting"] = (params or {})["edgeRouting"] made_boxes: set[str] = set() - for uid, (source, target) in get_all_classes(diagram.target): - property_uuid, text, partition = uid.split(" ")[1:4] + for uid, (source, prop, target) in get_all_classes(diagram.target): + partition = uid.split(" ")[-1] if target.uuid not in made_boxes: made_boxes.add(target.uuid) box = makers.make_box(target) @@ -44,15 +44,15 @@ def collector( box["layoutOptions"]["elk.partitioning.partition"] = int(partition) data["children"].append(box) - width, height = helpers.extent_func(text) + width, height = helpers.extent_func(prop.name) label: _elkjs.ELKInputLabel = { - "text": text, + "text": prop.name, "width": width + 2 * makers.LABEL_HPAD, "height": height + 2 * makers.LABEL_VPAD, } data["edges"].append( { - "id": property_uuid, + "id": prop.uuid, "sources": [source.uuid], "targets": [target.uuid], "labels": [label], @@ -61,18 +61,22 @@ def collector( return data +ClassContext = tuple[ + information.Class, information.Property, information.Class +] + + def get_all_classes( root: information.Class, partition: int = 0 -) -> cabc.Iterator[tuple[str, tuple[information.Class, information.Class]]]: +) -> cabc.Iterator[tuple[str, ClassContext]]: """Yield all classes of the class tree.""" partition += 1 - classes: dict[str, tuple[information.Class, information.Class]] = {} + classes: dict[str, ClassContext] = {} for prop in root.properties: if prop.type.xtype.endswith("Class"): - edge_id = f"{root.name} {prop.uuid} {prop.name}" - edge_id = f"{edge_id} {partition} {prop.type.name}" + edge_id = f"{root.name} {prop.type.name} {partition}" if edge_id not in classes: - classes[edge_id] = (root, prop.type) - classes.update(dict(get_all_classes(prop.type))) + classes[edge_id] = (root, prop, prop.type) + classes.update(dict(get_all_classes(prop.type, partition))) yield from classes.items() diff --git a/capellambse_context_diagrams/serializers.py b/capellambse_context_diagrams/serializers.py index 1f5d0b88..58f22389 100644 --- a/capellambse_context_diagrams/serializers.py +++ b/capellambse_context_diagrams/serializers.py @@ -28,7 +28,7 @@ * `junction`. """ -REMAP_STYLECLASS: dict[str, str] = {"Property": "Association"} +REMAP_STYLECLASS: dict[str, str] = {"Unset": "Association"} class DiagramSerializer: From 54a6b90de915b5ef83dc3c1b2d504b8eeb86b3fd Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 16 Oct 2023 15:05:09 +0200 Subject: [PATCH 3/9] test(class-tree): Add tests --- tests/data/ContextDiagram.aird | 215 +++++++++++++++++++++++++++++- tests/data/ContextDiagram.capella | 72 +++++++++- tests/test_class_tree_diagrams.py | 28 ++++ 3 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 tests/test_class_tree_diagrams.py diff --git a/tests/data/ContextDiagram.aird b/tests/data/ContextDiagram.aird index fbcacb6d..c60fd5c9 100644 --- a/tests/data/ContextDiagram.aird +++ b/tests/data/ContextDiagram.aird @@ -1,5 +1,5 @@ - + ContextDiagram.afm ContextDiagram.capella @@ -8,6 +8,10 @@ + + + + @@ -7978,4 +7982,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/ContextDiagram.capella b/tests/data/ContextDiagram.capella index bf35e631..0c76cd14 100644 --- a/tests/data/ContextDiagram.capella +++ b/tests/data/ContextDiagram.capella @@ -768,8 +768,78 @@ The predator is far away id="e3ccf45c-d714-40cd-9261-21f5b79f1a77" name="good advise" exchangeMechanism="FLOW"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3093,7 +3163,7 @@ The predator is far away + name="Hierarchy" abstractType="#16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb"/> None: + obj = model.by_uuid(CLASS_UUID) + + diag = obj.class_tree_diagram + + assert diag.render(fmt) + + +def test_class_tree_diagram_renders_with_additional_params( + model: capellambse.MelodyModel, +) -> None: + obj = model.by_uuid(CLASS_UUID) + + diag = obj.class_tree_diagram + + assert diag.render("svgdiagram", edgeRouting="POLYLINE", direction="RIGHT") From 9e4a048bc182611ec238bc2da4d937400982b6f3 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 16 Oct 2023 17:54:12 +0200 Subject: [PATCH 4/9] fix(class-tree): Fix duplicated edges and param test Also partitioning is now parametrized and can be controlled via render parameters. --- .../collectors/class_tree.py | 24 ++++++++++++------- capellambse_context_diagrams/context.py | 1 + tests/test_class_tree_diagrams.py | 13 +++++++++- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/capellambse_context_diagrams/collectors/class_tree.py b/capellambse_context_diagrams/collectors/class_tree.py index eab5e98e..64b93653 100644 --- a/capellambse_context_diagrams/collectors/class_tree.py +++ b/capellambse_context_diagrams/collectors/class_tree.py @@ -25,23 +25,29 @@ def collector( diagram: context.ContextDiagram, params: dict[str, t.Any] | None = None ) -> _elkjs.ELKInputData: """Return the class tree data for ELK.""" + params = params or {} assert isinstance(diagram.target, information.Class) data = generic.collector(diagram, no_symbol=True) - data["children"][0]["layoutOptions"] = {} - data["children"][0]["layoutOptions"]["elk.partitioning.partition"] = 0 + if params.get("partitioning", False): + data["children"][0]["layoutOptions"] = {} + data["children"][0]["layoutOptions"]["elk.partitioning.partition"] = 0 data["layoutOptions"] = LAYOUT_OPTIONS data["layoutOptions"]["algorithm"] = (params or {})["algorithm"] data["layoutOptions"]["elk.direction"] = (params or {})["direction"] data["layoutOptions"]["edgeRouting"] = (params or {})["edgeRouting"] made_boxes: set[str] = set() - for uid, (source, prop, target) in get_all_classes(diagram.target): - partition = uid.split(" ")[-1] + for _, (source, prop, target, partition) in get_all_classes( + diagram.target + ): if target.uuid not in made_boxes: made_boxes.add(target.uuid) box = makers.make_box(target) - box["layoutOptions"] = {} - box["layoutOptions"]["elk.partitioning.partition"] = int(partition) + if params.get("partitioning", False): + box["layoutOptions"] = {} + box["layoutOptions"]["elk.partitioning.partition"] = int( + partition + ) data["children"].append(box) width, height = helpers.extent_func(prop.name) @@ -62,7 +68,7 @@ def collector( ClassContext = tuple[ - information.Class, information.Property, information.Class + information.Class, information.Property, information.Class, int ] @@ -74,9 +80,9 @@ def get_all_classes( classes: dict[str, ClassContext] = {} for prop in root.properties: if prop.type.xtype.endswith("Class"): - edge_id = f"{root.name} {prop.type.name} {partition}" + edge_id = f"{root.uuid} {prop.uuid} {prop.type.uuid}" if edge_id not in classes: - classes[edge_id] = (root, prop, prop.type) + classes[edge_id] = (root, prop, prop.type, partition) classes.update(dict(get_all_classes(prop.type, partition))) yield from classes.items() diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 64d1d2f4..53ca7ce8 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -350,5 +350,6 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: params.setdefault("algorithm", "layered") params.setdefault("direction", "DOWN") params.setdefault("edgeRouting", "POLYLINE") + params.setdefault("partitioning", True) params["elkdata"] = class_tree.collector(self, params) return super()._create_diagram(params) diff --git a/tests/test_class_tree_diagrams.py b/tests/test_class_tree_diagrams.py index 55200fcb..98a2b48f 100644 --- a/tests/test_class_tree_diagrams.py +++ b/tests/test_class_tree_diagrams.py @@ -18,11 +18,22 @@ def test_class_tree_diagram_gets_rendered_successfully( assert diag.render(fmt) +@pytest.mark.parametrize("edgeRouting", ["SPLINE", "ORTHOGONAL", "POLYLINE"]) +@pytest.mark.parametrize("direction", ["DOWN", "RIGHT"]) +@pytest.mark.parametrize("partitioning", [True, False]) def test_class_tree_diagram_renders_with_additional_params( model: capellambse.MelodyModel, + edgeRouting: str, + direction: str, + partitioning: bool, ) -> None: obj = model.by_uuid(CLASS_UUID) diag = obj.class_tree_diagram - assert diag.render("svgdiagram", edgeRouting="POLYLINE", direction="RIGHT") + assert diag.render( + "svgdiagram", + edgeRouting=edgeRouting, + direction=direction, + partitioning=partitioning, + ) From 6e9964ffab334c7590cfe96840b99108c7fd5347 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 18 Oct 2023 14:31:40 +0200 Subject: [PATCH 5/9] docs(class-tree): Document the class-tree diagram feature --- docs/class-tree.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++ docs/gen_images.py | 21 ++++++++++++++ mkdocs.yml | 2 ++ 3 files changed, 95 insertions(+) create mode 100644 docs/class-tree.md diff --git a/docs/class-tree.md b/docs/class-tree.md new file mode 100644 index 00000000..13ba69ed --- /dev/null +++ b/docs/class-tree.md @@ -0,0 +1,72 @@ + + +# Class Tree Diagram + +With release [`v0.5.35`](https://github.com/DSD-DBS/py-capellambse/releases/tag/v0.5.35) of [py-capellambse](https://github.com/DSD-DBS/py-capellambse) you can access the +`.class_tree_diagram` on `Class` objects. A class tree diagram shows a tree +made from all properties of the parent class. + +??? example "Class Tree of Root" + + ``` py + import capellambse + + model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") + diag = model.by_uuid("b7c7f442-377f-492c-90bf-331e66988bda").class_tree_diagram + diag.render("svgdiagram").save_drawing(pretty=True) + ``` +
+ +
[CDB] Class Tree Diagram of Root
+
+ +Additional rendering parameters enable the control over the layout computed by +ELK. The available options are: + +1. edgeRouting - Controls the style of the edges. + - POLYLINE (default) + - ORTHOGONAL + - SPLINE +2. algorithm - Controls the algorithm for the diagram layout. + - layered (default) + - mr.tree + - ... Have a look for [all available ELK algorithms](https://eclipse.dev/elk/reference/algorithms.html). +3. direction - The flow direction for the ELK Layered algortihm. + - DOWN (DEFAULT) + - UP + - RIGHT + - LEFT +4. partitioning - Enable partitioning. Each recursion level for collecting the +classes is its own partition. + - True (default) + - False + +Here is an example that shows how convenient these parameters can be passed +before rendering: + +??? example "Class Tree of Root" + + ``` py + import capellambse + + model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") + diag = model.by_uuid("b7c7f442-377f-492c-90bf-331e66988bda").class_tree_diagram + diag.render( + "svgdiagram", + edgeRouting="ORTHOGONAL", + direction="Right", + partitioning=False, + ).save_drawing(pretty=True) + ``` +
+ +
[CDB] Class Tree Diagram of Root
+
+ +## Check out the code + +To understand the collection have a look into the +[`class_tree`][capellambse_context_diagrams.collectors.class_tree] module. diff --git a/docs/gen_images.py b/docs/gen_images.py index a3432a21..ed2ed843 100644 --- a/docs/gen_images.py +++ b/docs/gen_images.py @@ -32,6 +32,7 @@ } hierarchy_context = "16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb" diagram_uuids = general_context_diagram_uuids | interface_context_diagram_uuids +class_tree_uuid = "b7c7f442-377f-492c-90bf-331e66988bda" def generate_index_images() -> None: @@ -91,6 +92,25 @@ def generate_hierarchy_image() -> None: print(diag.render("svg", include_inner_objects=True), file=fd) +def generate_class_tree_images() -> None: + obj = model.by_uuid(class_tree_uuid) + diag = obj.class_tree_diagram + with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd: + print(diag.render("svg"), file=fd) + with mkdocs_gen_files.open( + f"{str(dest / diag.name)}-params.svg", "w" + ) as fd: + print( + diag.render( + "svg", + edgeRouting="ORTHOGONAL", + direction="Right", + partitioning=False, + ), + file=fd, + ) + + generate_index_images() generate_hierarchy_image() generate_no_symbol_images() @@ -112,3 +132,4 @@ def generate_hierarchy_image() -> None: "red junction", ) generate_styling_image(wizard, {}, "no_styles") +generate_class_tree_images() diff --git a/mkdocs.yml b/mkdocs.yml index 5340a0eb..de5e508a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -89,6 +89,8 @@ nav: - Extras: - Filters: extras/filters.md - Styling: extras/styling.md + - Class Tree: + - Overview: class-tree.md - Code Reference: reference/ extra_css: From 8f85612a7b3150d82f6b84115f3c4fa1693b98f0 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 18 Oct 2023 14:32:53 +0200 Subject: [PATCH 6/9] docs: Fix `save_drawing` bug --- CONTRIBUTING.md | 2 +- docs/extras/filters.md | 6 +++--- docs/extras/styling.md | 12 ++++++------ docs/index.md | 20 ++++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 716b95fd..1075dc65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,7 +150,7 @@ Example: > > model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") > diag = model.by_uuid("5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6").context_diagram -> diag.render("svgdiagram").save_drawing(True) +> diag.render("svgdiagram").save_drawing(pretty=True) > ``` >
> diff --git a/docs/extras/filters.md b/docs/extras/filters.md index 26019f93..f6f60e69 100644 --- a/docs/extras/filters.md +++ b/docs/extras/filters.md @@ -26,7 +26,7 @@ Currently the supported filters are: diag = obj.context_diagram assert filters.EX_ITEMS == "show.exchange.items.filter" diag.filters.add(filters.EX_ITEMS) - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -43,7 +43,7 @@ Currently the supported filters are: diag = obj.context_diagram assert filters.FEX_EX_ITEMS == "show.functional.exchanges.exchange.items.filter" filters.filters = {filters.FEX_EX_ITEMS} - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -62,7 +62,7 @@ Currently the supported filters are: diag = obj.context_diagram assert filters.FEX_OR_EX_ITEMS == "capellambse_context_diagrams-show.functional.exchanges.or.exchange.items.filter" filters.filters.add(filters.FEX_OR_EX_ITEMS) - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
diff --git a/docs/extras/styling.md b/docs/extras/styling.md index 343b0254..2e2400bb 100644 --- a/docs/extras/styling.md +++ b/docs/extras/styling.md @@ -22,7 +22,7 @@ Capella. These appear to be blue. model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("957c5799-1d4a-4ac0-b5de-33a65bf1519c").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ``` produces
@@ -47,7 +47,7 @@ displayed as an icon beside the box-label. diag = model.by_uuid("da08ddb6-92ba-4c3b-956a-017424dbfe85").context_diagram diag.display_symbols_as_boxes = True - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ``` produces
@@ -62,7 +62,7 @@ displayed as an icon beside the box-label. diag = model.by_uuid("9390b7d5-598a-42db-bef8-23677e45ba06").context_diagram diag.display_symbols_as_boxes = True - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ``` produces
@@ -81,7 +81,7 @@ The `no_edgelabels` render parameter prevents edge labels from being displayed. model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("957c5799-1d4a-4ac0-b5de-33a65bf1519c").context_diagram - diag.render("svgdiagram", no_edgelabels=True).save_drawing(True) + diag.render("svgdiagram", no_edgelabels=True).save_drawing(pretty=True) ```
@@ -101,7 +101,7 @@ You can switch to py-capellambse default styling by overriding the diag = model.by_uuid("957c5799-1d4a-4ac0-b5de-33a65bf1519c").context_diagram diag.render_styles = {} - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ``` produces
@@ -126,7 +126,7 @@ Style your diagram elements ([ElkChildType][capellambse_context_diagrams.seriali styling.BLUE_ACTOR_FNCS, junction=lambda obj, serializer: {"fill": aird.RGB(220, 20, 60)}, ) - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ``` produces
diff --git a/docs/index.md b/docs/index.md index 8134c415..3a75e2ee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -41,7 +41,7 @@ Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.e model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("e37510b9-3166-4f80-a919-dfaac9b696c7").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -55,7 +55,7 @@ Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.e model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("8bcb11e6-443b-4b92-bec2-ff1d87a224e7").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -69,7 +69,7 @@ Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.e model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("da08ddb6-92ba-4c3b-956a-017424dbfe85").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -83,7 +83,7 @@ Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.e model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -97,7 +97,7 @@ Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.e model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("9390b7d5-598a-42db-bef8-23677e45ba06").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -113,7 +113,7 @@ Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.e model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("a5642060-c9cc-4d49-af09-defaa3024bae").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -127,7 +127,7 @@ Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.e model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("f632888e-51bc-4c9f-8e81-73e9404de784").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -141,7 +141,7 @@ Available via `.context_diagram` on a [`ModelObject`][capellambse.model.common.e model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("957c5799-1d4a-4ac0-b5de-33a65bf1519c").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -167,7 +167,7 @@ Hierarchy is identified and supported: model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") obj = model.by_uuid("16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb") diagram = obj.context_diagram.render("svgdiagram", include_inner_objects=True) - diagram.save_drawing(True) + diagram.save_drawing(pretty=True) ```
@@ -185,7 +185,7 @@ The data is collected by [get_elkdata_for_exchanges][capellambse_context_diagram model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") diag = model.by_uuid("3ef23099-ce9a-4f7d-812f-935f47e7938d").context_diagram - diag.render("svgdiagram").save_drawing(True) + diag.render("svgdiagram").save_drawing(pretty=True) ```
From 2dfb59ad72c030886fe3f420bb0ea8f21b77b154 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 18 Oct 2023 19:38:41 +0200 Subject: [PATCH 7/9] refactor(class-tree): Resolve suggestions from code review - Rename attribute to just `tree_diagram` - Remove the `LAYOUT_OPTIONS` global dict - Add controlability of `edgeLabels.sideSelection` --- capellambse_context_diagrams/__init__.py | 4 +-- .../collectors/class_tree.py | 25 ++++++++----------- capellambse_context_diagrams/context.py | 1 + docs/class-tree.md | 17 ++++++++++--- docs/gen_images.py | 3 ++- tests/test_class_tree_diagrams.py | 13 +++++++--- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/capellambse_context_diagrams/__init__.py b/capellambse_context_diagrams/__init__.py index f0d52bd6..3d496816 100644 --- a/capellambse_context_diagrams/__init__.py +++ b/capellambse_context_diagrams/__init__.py @@ -152,9 +152,9 @@ def register_functional_context() -> None: def register_class_tree() -> None: - """Add the `class_tree_diagram` attribute to `ModelObject`s.""" + """Add the `tree_diagram` attribute to ``Class``es.""" common.set_accessor( information.Class, - "class_tree_diagram", + "tree_diagram", context.ClassTreeAccessor(DiagramType.CDB.value), ) diff --git a/capellambse_context_diagrams/collectors/class_tree.py b/capellambse_context_diagrams/collectors/class_tree.py index 64b93653..009913de 100644 --- a/capellambse_context_diagrams/collectors/class_tree.py +++ b/capellambse_context_diagrams/collectors/class_tree.py @@ -12,29 +12,26 @@ from .. import _elkjs, context from . import generic, makers -LAYOUT_OPTIONS: _elkjs.LayoutOptions = { - "algorithm": "layered", - "edgeRouting": "ORTHOGONAL", - "elk.direction": "DOWN", - "partitioning.activate": True, - "edgeLabels.sideSelection": "ALWAYS_DOWN", -} - def collector( - diagram: context.ContextDiagram, params: dict[str, t.Any] | None = None + diagram: context.ContextDiagram, params: dict[str, t.Any] ) -> _elkjs.ELKInputData: """Return the class tree data for ELK.""" - params = params or {} assert isinstance(diagram.target, information.Class) data = generic.collector(diagram, no_symbol=True) if params.get("partitioning", False): + data["layoutOptions"]["partitioning.activate"] = True data["children"][0]["layoutOptions"] = {} data["children"][0]["layoutOptions"]["elk.partitioning.partition"] = 0 - data["layoutOptions"] = LAYOUT_OPTIONS - data["layoutOptions"]["algorithm"] = (params or {})["algorithm"] - data["layoutOptions"]["elk.direction"] = (params or {})["direction"] - data["layoutOptions"]["edgeRouting"] = (params or {})["edgeRouting"] + + data["layoutOptions"]["edgeLabels.sideSelection"] = params.get( + "edgeLabelsSide", "ALWAYS_DOWN" + ) + data["layoutOptions"]["algorithm"] = params.get("algorithm", "layered") + data["layoutOptions"]["elk.direction"] = params.get("direction", "DOWN") + data["layoutOptions"]["edgeRouting"] = params.get( + "edgeRouting", "ORTHOGONAL" + ) made_boxes: set[str] = set() for _, (source, prop, target, partition) in get_all_classes( diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 53ca7ce8..536f888a 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -351,5 +351,6 @@ def _create_diagram(self, params: dict[str, t.Any]) -> cdiagram.Diagram: params.setdefault("direction", "DOWN") params.setdefault("edgeRouting", "POLYLINE") params.setdefault("partitioning", True) + params.setdefault("edgeLabelsSide", "SMART_DOWN") params["elkdata"] = class_tree.collector(self, params) return super()._create_diagram(params) diff --git a/docs/class-tree.md b/docs/class-tree.md index 13ba69ed..4c55d44b 100644 --- a/docs/class-tree.md +++ b/docs/class-tree.md @@ -6,8 +6,9 @@ # Class Tree Diagram With release [`v0.5.35`](https://github.com/DSD-DBS/py-capellambse/releases/tag/v0.5.35) of [py-capellambse](https://github.com/DSD-DBS/py-capellambse) you can access the -`.class_tree_diagram` on `Class` objects. A class tree diagram shows a tree -made from all properties of the parent class. +`.tree_diagram` on [`Class`][capellambse.model.crosslayer.information.Class] +objects. A class tree diagram shows a tree made from all properties of the +parent class. ??? example "Class Tree of Root" @@ -15,7 +16,7 @@ made from all properties of the parent class. import capellambse model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") - diag = model.by_uuid("b7c7f442-377f-492c-90bf-331e66988bda").class_tree_diagram + diag = model.by_uuid("b7c7f442-377f-492c-90bf-331e66988bda").tree_diagram diag.render("svgdiagram").save_drawing(pretty=True) ```
@@ -43,6 +44,13 @@ ELK. The available options are: classes is its own partition. - True (default) - False +5. edgeLabelSide - Controls edge label placement. + - SMART_DOWN (default) + - SMART_UP + - ALWAYS_UP + - ALWAYS_DOWN + - DIRECTION_UP + - DIRECTION_DOWN Here is an example that shows how convenient these parameters can be passed before rendering: @@ -53,12 +61,13 @@ before rendering: import capellambse model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") - diag = model.by_uuid("b7c7f442-377f-492c-90bf-331e66988bda").class_tree_diagram + diag = model.by_uuid("b7c7f442-377f-492c-90bf-331e66988bda").tree_diagram diag.render( "svgdiagram", edgeRouting="ORTHOGONAL", direction="Right", partitioning=False, + edgeLabelsSide="ALWAYS_DOWN", ).save_drawing(pretty=True) ```
diff --git a/docs/gen_images.py b/docs/gen_images.py index ed2ed843..f9004f57 100644 --- a/docs/gen_images.py +++ b/docs/gen_images.py @@ -94,7 +94,7 @@ def generate_hierarchy_image() -> None: def generate_class_tree_images() -> None: obj = model.by_uuid(class_tree_uuid) - diag = obj.class_tree_diagram + diag = obj.tree_diagram with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd: print(diag.render("svg"), file=fd) with mkdocs_gen_files.open( @@ -106,6 +106,7 @@ def generate_class_tree_images() -> None: edgeRouting="ORTHOGONAL", direction="Right", partitioning=False, + edgeLabelsSide="ALWAYS_DOWN", ), file=fd, ) diff --git a/tests/test_class_tree_diagrams.py b/tests/test_class_tree_diagrams.py index 98a2b48f..66e20866 100644 --- a/tests/test_class_tree_diagrams.py +++ b/tests/test_class_tree_diagrams.py @@ -8,12 +8,12 @@ @pytest.mark.parametrize("fmt", ["svgdiagram", "svg", None]) -def test_class_tree_diagram_gets_rendered_successfully( +def test_tree_diagram_gets_rendered_successfully( model: capellambse.MelodyModel, fmt: str ) -> None: obj = model.by_uuid(CLASS_UUID) - diag = obj.class_tree_diagram + diag = obj.tree_diagram assert diag.render(fmt) @@ -21,19 +21,24 @@ def test_class_tree_diagram_gets_rendered_successfully( @pytest.mark.parametrize("edgeRouting", ["SPLINE", "ORTHOGONAL", "POLYLINE"]) @pytest.mark.parametrize("direction", ["DOWN", "RIGHT"]) @pytest.mark.parametrize("partitioning", [True, False]) -def test_class_tree_diagram_renders_with_additional_params( +@pytest.mark.parametrize( + "edgeLabelsSide", ["ALWAYS_DOWN", "DIRECTION_DOWN", "SMART_DOWN"] +) +def test_tree_diagram_renders_with_additional_params( model: capellambse.MelodyModel, edgeRouting: str, direction: str, partitioning: bool, + edgeLabelsSide: str, ) -> None: obj = model.by_uuid(CLASS_UUID) - diag = obj.class_tree_diagram + diag = obj.tree_diagram assert diag.render( "svgdiagram", edgeRouting=edgeRouting, direction=direction, partitioning=partitioning, + edgeLabelsSide=edgeLabelsSide, ) From f655fdb0efce9e544ac37e770881c1a37f92d1af Mon Sep 17 00:00:00 2001 From: ewuerger Date: Fri, 20 Oct 2023 09:56:23 +0200 Subject: [PATCH 8/9] docs(class-tree): Hint about optional render params --- docs/class-tree.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/class-tree.md b/docs/class-tree.md index 4c55d44b..40709290 100644 --- a/docs/class-tree.md +++ b/docs/class-tree.md @@ -66,8 +66,8 @@ before rendering: "svgdiagram", edgeRouting="ORTHOGONAL", direction="Right", - partitioning=False, - edgeLabelsSide="ALWAYS_DOWN", + # partitioning=False, + # edgeLabelsSide="ALWAYS_DOWN", ).save_drawing(pretty=True) ```
@@ -75,6 +75,8 @@ before rendering:
[CDB] Class Tree Diagram of Root
+They are optional and don't need to be set all together. + ## Check out the code To understand the collection have a look into the From 2037d68302e12a669e13729ca3f948e3cadd64b5 Mon Sep 17 00:00:00 2001 From: Martin Lehmann Date: Mon, 11 Sep 2023 11:17:37 +0200 Subject: [PATCH 9/9] chore: Update pre-commit hooks Also switch to the pre-commit black mirror, which runs about 2x faster. --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 64699dcd..4bd9e976 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ exclude: '^(versioneer\.py|.*/_version\.py)$' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-ast @@ -24,8 +24,8 @@ repos: - id: end-of-file-fixer - id: fix-byte-order-marker - id: trailing-whitespace - - repo: https://github.com/psf/black - rev: 23.3.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.10.0 hooks: - id: black - repo: https://github.com/PyCQA/isort @@ -33,7 +33,7 @@ repos: hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.6.1 hooks: - id: mypy - repo: https://github.com/Lucas-C/pre-commit-hooks @@ -67,6 +67,6 @@ repos: - --comment-style - '/*| *| */' - repo: https://github.com/fsfe/reuse-tool - rev: v1.1.2 + rev: v2.1.0 hooks: - id: reuse