Issue #24 - Use ELK layout to position SVG tasks + links - #30
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
036190d to
dd4db3c
Compare
There was a problem hiding this comment.
When applying ELK layout routing computation:
elk_graph = ELK().layout(elk_graph)The ElkGraph is modified in place, adding metadata about positioning, links, routing ...
In this version of the code, I'm creating two types:
- the "uncomputed" graph :
ElkGraph. - the "computed" graph:
LaidOutElkGraph
To have something explicit like that:
elk_graph: ElkGraph = convert_ewoks_to_elk_graph( ... )
laid_out_graph: LaidOutElkGraph = ELK().layout(elk_graph)But the definition here is a little verbose.
Another solution is to go for a single type with NotRequired elements:
class ElkChild(TypedDict):
x: NotRequired[float]
y: NotRequired[float]elk_graph: ElkGraph = convert_ewoks_to_elk_graph( ... )
laid_out_graph: elk_graph = ELK().layout(elk_graph)What do you think ?
There was a problem hiding this comment.
I like the two definitions. Sure it is more verbose but it is very clear in terms with what we expect. Since we are enforcing typing with mypy, I think it will help in the long run.
There was a problem hiding this comment.
The __init__.py is getting crowded with various SVG group builders.
I'm moving these helper functions to ./layout/.
There was a problem hiding this comment.
I'll even suggest to move everything from __init__.py in another PR. Personally, I am so used to have nothing in these files but I don't always have to idea to check it.
Moving functions in a new module also creates the opportunity to describe a bit more what the functions are for, though the module name.
| if len(points) < 2: | ||
| continue | ||
|
|
||
| cubic_bezier_path = CubicBezierPath.from_points( |
There was a problem hiding this comment.
This is the mathematical representation of the Bezier Path.
We make the orthogonal control points into smooth curved curves.
| @pytest.fixture(params=graph_names(), ids=graph_names()) | ||
| def ewoks_graph(request: pytest.FixtureRequest) -> TaskGraph: | ||
| graph_description, _ = get_graph(request.param) | ||
| return load_graph(graph_description) |
There was a problem hiding this comment.
So far I was only testing on a single graph.
Why not test all of them?
| task_group.extract_input_positions(), | ||
| task_group.extract_output_positions(), | ||
| ) | ||
| laid_out_graph: LaidOutElkGraph = ELK().layout(elk_graph) |
There was a problem hiding this comment.
The layout routing is happening here.
| task_positions = extract_task_positions_from_elk_graph(laid_out_graph) | ||
| task_group.set_task_positions(task_positions) |
There was a problem hiding this comment.
I want to keep the ELK graph representation separated from the SVG structure.
| class ElkChildBase(TypedDict): | ||
| id: str | ||
| width: float | ||
| height: float | ||
| layoutOptions: dict[str, Any] | ||
| ports: list[ElkPort] | ||
|
|
||
|
|
||
| class ElkEdge(TypedDict): | ||
| class ElkChild(ElkChildBase): | ||
| """An ELK child before layout.""" | ||
|
|
||
|
|
||
| class LaidOutElkChild(ElkChildBase): | ||
| """An ELK child with coordinates computed by ELK.""" | ||
|
|
||
| x: float | ||
| y: float |
There was a problem hiding this comment.
Two comments:
- If the base is the same as one of the class, I don't think we need to keep both.
- Since the first-class citizen is the laid-out graph, I would propose to rename elements before layout to
ElementBeforeLayoutorUnpositionnedElementand keep the "proper" name for the laid out elementElement.
Here how it would pan out for ElkChild:
| class ElkChildBase(TypedDict): | |
| id: str | |
| width: float | |
| height: float | |
| layoutOptions: dict[str, Any] | |
| ports: list[ElkPort] | |
| class ElkEdge(TypedDict): | |
| class ElkChild(ElkChildBase): | |
| """An ELK child before layout.""" | |
| class LaidOutElkChild(ElkChildBase): | |
| """An ELK child with coordinates computed by ELK.""" | |
| x: float | |
| y: float | |
| class ElkChildBeforeLayout(TypedDict): | |
| id: str | |
| width: float | |
| height: float | |
| layoutOptions: dict[str, Any] | |
| ports: list[ElkPort] | |
| class ElkChild(ElkChildBeforeLayout): | |
| """An ELK child with coordinates computed by ELK.""" | |
| x: float | |
| y: float |
|
|
||
|
|
||
| class ElkGraph(ElkGraphBase): | ||
| """An ELK graph before layout.""" |
There was a problem hiding this comment.
I'd suggest to separate the types in two parts: one for raw, unpositionned elements and the other for laid out elements.
There was a problem hiding this comment.
I like the two definitions. Sure it is more verbose but it is very clear in terms with what we expect. Since we are enforcing typing with mypy, I think it will help in the long run.
There was a problem hiding this comment.
I'll even suggest to move everything from __init__.py in another PR. Personally, I am so used to have nothing in these files but I don't always have to idea to check it.
Moving functions in a new module also creates the opportunity to describe a bit more what the functions are for, though the module name.
| @@ -0,0 +1,56 @@ | |||
| from ewoksdraw.config.constants import LINK_TURN_RADIUS | |||
There was a problem hiding this comment.
| from ewoksdraw.config.constants import LINK_TURN_RADIUS | |
| from .config.constants import LINK_TURN_RADIUS |
| if len(points) < 2: | ||
| continue |
There was a problem hiding this comment.
By design, points length is always >=2 no?
There was a problem hiding this comment.
Not sure this is the best place nor the best name for this module.
Should it not be in svg ? Or even put the function in svg_task_group?
| from .svg_task import TaskSize | ||
|
|
||
| TaskSizes = dict[str, TaskSize] | ||
| TaskPositions = list[TaskPosition] |
There was a problem hiding this comment.
Why about making this a dict[str, TaskPosition] ? Then, we ensure by design that the name is used as id.
| transform = group.get("transform") | ||
| assert transform is not None |
There was a problem hiding this comment.
If transform is needed, we may as well let the KeyError be raised:
| transform = group.get("transform") | |
| assert transform is not None | |
| transform = group["transform"] |
| # Going through the source/target couples in ewoks graph edges | ||
| # checking for each the computed ELK positions | ||
| # gives source on the left of target | ||
| assert positions[source][0] < positions[target][0] |
There was a problem hiding this comment.
Already part of the docstring
| # Going through the source/target couples in ewoks graph edges | |
| # checking for each the computed ELK positions | |
| # gives source on the left of target | |
| assert positions[source][0] < positions[target][0] | |
| assert positions[source][0] < positions[target][0] |
PR summary
From the graph/tasks geometry ELK representation, we now used ELK to compute the tasks' optimal positions and link routing. The computed ELK layout is used to position the tasks and draw the links.
AI Disclosure