Skip to content

Commit bf518bb

Browse files
Merge remote-tracking branch 'upstream/hotfixes' into release
2 parents 7e8e413 + 29d8101 commit bf518bb

File tree

5 files changed

+102
-44
lines changed

5 files changed

+102
-44
lines changed

pm4py/meta.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
Contact: info@processintelligence.solutions
2121
'''
2222
__name__ = "pm4py"
23-
VERSION = "2.7.19.1"
23+
VERSION = "2.7.19.2"
2424

2525
__version__ = VERSION
2626
__doc__ = "Process mining for Python"

pm4py/objects/bpmn/layout/variants/graphviz_new.py

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,59 @@
2020
Contact: info@processintelligence.solutions
2121
'''
2222
from pm4py.objects.bpmn.obj import BPMN
23-
from typing import Optional, Dict, Any
24-
23+
from typing import Optional, Dict, Any, Tuple, List
2524
from copy import copy
2625
import tempfile
2726

2827

28+
def _get_node_center(layout, node) -> Tuple[float, float]:
29+
"""Get the center point of a node."""
30+
node_layout = layout.get(node)
31+
x = node_layout.get_x() + node_layout.get_width() / 2
32+
y = node_layout.get_y() + node_layout.get_height() / 2
33+
return (x, y)
34+
35+
36+
def _get_node_boundary_point(layout, node, target_point: Tuple[float, float]) -> Tuple[float, float]:
37+
"""
38+
Calculate the intersection point on the node boundary
39+
towards a target point.
40+
"""
41+
node_layout = layout.get(node)
42+
x = node_layout.get_x()
43+
y = node_layout.get_y()
44+
w = node_layout.get_width()
45+
h = node_layout.get_height()
46+
47+
cx, cy = x + w / 2, y + h / 2
48+
tx, ty = target_point
49+
50+
dx = tx - cx
51+
dy = ty - cy
52+
53+
if dx == 0 and dy == 0:
54+
return (cx, y) # Default to top edge
55+
56+
# Calculate intersection with rectangle boundary
57+
if dx == 0:
58+
return (cx, y if dy < 0 else y + h)
59+
if dy == 0:
60+
return (x if dx < 0 else x + w, cy)
61+
62+
# Check intersection with each edge
63+
scale_x = (w / 2) / abs(dx)
64+
scale_y = (h / 2) / abs(dy)
65+
scale = min(scale_x, scale_y)
66+
67+
return (cx + dx * scale, cy + dy * scale)
68+
69+
2970
def apply(
30-
bpmn_graph: BPMN, parameters: Optional[Dict[Any, Any]] = None
71+
bpmn_graph: BPMN, parameters: Optional[Dict[Any, Any]] = None
3172
) -> BPMN:
3273
"""
33-
Layouts the BPMN graphviz using directly the information about node positioning
34-
and edges waypoints provided in the SVG obtained from Graphviz.
35-
36-
Parameters
37-
-----------------
38-
bpmn_graph
39-
BPMN graph
40-
parameters
41-
Optional parameters of the method
42-
43-
Returns
44-
----------------
45-
layouted_bpmn
46-
Layouted BPMN
74+
Layouts the BPMN graph using node positioning and edge waypoints from Graphviz SVG.
75+
Ensures edges connect properly to node boundaries.
4776
"""
4877
if parameters is None:
4978
parameters = {}
@@ -53,7 +82,7 @@ def apply(
5382

5483
layout = bpmn_graph.get_layout()
5584

56-
filename_svg = tempfile.NamedTemporaryFile(suffix=".svg")
85+
filename_svg = tempfile.NamedTemporaryFile(suffix=".svg", delete=False)
5786
filename_svg.close()
5887

5988
vis_parameters = copy(parameters)
@@ -64,36 +93,45 @@ def apply(
6493
gviz = bpmn_visualizer.apply(bpmn_graph, parameters=vis_parameters)
6594
bpmn_visualizer.save(gviz, filename_svg.name)
6695

67-
# print(filename_svg.name)
68-
6996
nodes_p, edges_p = svg_pos_parser.apply(filename_svg.name)
7097

98+
# First pass: layout all nodes
7199
for node in list(bpmn_graph.get_nodes()):
72100
node_id = str(id(node))
73101
if node_id in nodes_p:
74102
node_info = nodes_p[node_id]
75103
if node_info["polygon"] is not None:
76-
min_x = min(x[0] for x in node_info["polygon"])
77-
max_x = max(x[0] for x in node_info["polygon"])
78-
min_y = min(x[1] for x in node_info["polygon"])
79-
max_y = max(x[1] for x in node_info["polygon"])
104+
min_x = min(p[0] for p in node_info["polygon"])
105+
max_x = max(p[0] for p in node_info["polygon"])
106+
min_y = min(p[1] for p in node_info["polygon"])
107+
max_y = max(p[1] for p in node_info["polygon"])
80108

81-
width = max_x - min_x
82-
height = max_y - min_y
83-
84-
layout.get(node).set_width(width)
85-
layout.get(node).set_height(height)
109+
layout.get(node).set_width(max_x - min_x)
110+
layout.get(node).set_height(max_y - min_y)
86111
layout.get(node).set_x(min_x)
87112
layout.get(node).set_y(min_y)
88113

114+
# Second pass: layout edges with proper connection points
89115
for flow in list(bpmn_graph.get_flows()):
90116
flow_id = (str(id(flow.source)), str(id(flow.target)))
91117
if flow_id in edges_p:
92118
flow_info = edges_p[flow_id]
93-
if flow_info["waypoints"] is not None:
119+
if flow_info["waypoints"] is not None and len(flow_info["waypoints"]) >= 2:
94120
flow.del_waypoints()
95121

96-
for wayp in flow_info["waypoints"]:
122+
waypoints = list(flow_info["waypoints"])
123+
124+
# Adjust start point to source node boundary
125+
if len(waypoints) > 1:
126+
start_boundary = _get_node_boundary_point(layout, flow.source, waypoints[1])
127+
waypoints[0] = start_boundary
128+
129+
# Adjust end point to target node boundary
130+
if len(waypoints) > 1:
131+
end_boundary = _get_node_boundary_point(layout, flow.target, waypoints[-2])
132+
waypoints[-1] = end_boundary
133+
134+
for wayp in waypoints:
97135
flow.add_waypoint(wayp)
98136

99137
return bpmn_graph

pm4py/vis.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1989,6 +1989,7 @@ def view_powl(
19891989
format: str = constants.DEFAULT_FORMAT_GVIZ_VIEW,
19901990
bgcolor: str = "white",
19911991
variant_str: str = "basic",
1992+
rankdir: str = "TB",
19921993
graph_title: Optional[str] = None,
19931994
):
19941995
"""
@@ -2002,6 +2003,7 @@ def view_powl(
20022003
:param powl: POWL model
20032004
:param format: Format of the visualization (default: png)
20042005
:param bgcolor: Background color (default: white)
2006+
:param rankdir: Graph direction ("LR" or "TB")
20052007
:param variant_str: Variant of the visualization to be used ("basic" or "net")
20062008
:param graph_title: Title of the visualization (if provided)
20072009
@@ -2026,7 +2028,7 @@ def view_powl(
20262028
fmt = _extract_format(format)
20272029
from pm4py.visualization.powl import visualizer as powl_visualizer
20282030

2029-
params = _setup_parameters(fmt, bgcolor, graph_title=graph_title)
2031+
params = _setup_parameters(fmt, bgcolor, rankdir, graph_title=graph_title)
20302032
gviz = powl_visualizer.apply(powl, variant=variant, parameters=params)
20312033

20322034
powl_visualizer.view(gviz, parameters=params)

pm4py/visualization/powl/variants/basic.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Parameters(Enum):
5050
ENABLE_DEEPCOPY = "enable_deepcopy"
5151
FONT_SIZE = "font_size"
5252
BGCOLOR = "bgcolor"
53+
RANKDIR = "rankdir"
5354
ENABLE_GRAPH_TITLE = "enable_graph_title"
5455
GRAPH_TITLE = "graph_title"
5556

@@ -82,20 +83,25 @@ def apply(powl: POWL, parameters: Optional[Dict[Any, Any]] = None) -> Digraph:
8283

8384
filename = tempfile.NamedTemporaryFile(suffix=".gv")
8485

86+
rankdir = exec_utils.get_param_value(Parameters.RANKDIR, parameters, "TB")
87+
bgcolor = exec_utils.get_param_value(
88+
Parameters.BGCOLOR, parameters, fillcolor
89+
)
90+
8591
viz = Digraph("powl", filename=filename.name, engine="dot")
8692
viz.attr("node", shape="ellipse", fixedsize="false")
8793
viz.attr(nodesep="1")
8894
viz.attr(ranksep="1")
8995
viz.attr(compound="true")
9096
viz.attr(overlap="scale")
9197
viz.attr(splines="true")
92-
viz.attr(rankdir="TB")
98+
viz.attr(rankdir=rankdir)
9399
viz.attr(style="filled")
94-
viz.attr(fillcolor=fillcolor)
100+
viz.attr(fillcolor=bgcolor)
95101

96102
color_map = exec_utils.get_param_value(Parameters.COLOR_MAP, {}, {})
97103

98-
repr_powl(powl, viz, color_map, level=0)
104+
repr_powl(powl, viz, color_map, level=0, base_color=bgcolor)
99105
viz.format = "svg"
100106

101107
return viz
@@ -204,12 +210,12 @@ def add_order_edge(
204210
)
205211

206212

207-
def repr_powl(powl, viz, color_map, level):
213+
def repr_powl(powl, viz, color_map, level, base_color):
208214
font_size = "18"
209215
this_node_id = str(id(powl))
210216

211217
current_color = darken_color(
212-
fillcolor, amount=opacity_change_ratio * level
218+
base_color, amount=opacity_change_ratio * level
213219
)
214220

215221
if isinstance(powl, FrequentTransition):
@@ -305,7 +311,9 @@ def repr_powl(powl, viz, color_map, level):
305311
block.attr(style="filled")
306312
block.attr(fillcolor=current_color)
307313
for child in powl.children:
308-
repr_powl(child, block, color_map, level=level + 1)
314+
repr_powl(
315+
child, block, color_map, level=level + 1, base_color=base_color
316+
)
309317
for child in powl.children:
310318
for child2 in powl.children:
311319
if transitive_reduction.is_edge(child, child2):
@@ -332,9 +340,13 @@ def repr_powl(powl, viz, color_map, level):
332340
)
333341
do = powl.children[0]
334342
redo = powl.children[1]
335-
repr_powl(do, block, color_map, level=level + 1)
343+
repr_powl(
344+
do, block, color_map, level=level + 1, base_color=base_color
345+
)
336346
add_operator_edge(block, this_node_id, do)
337-
repr_powl(redo, block, color_map, level=level + 1)
347+
repr_powl(
348+
redo, block, color_map, level=level + 1, base_color=base_color
349+
)
338350
add_operator_edge(block, this_node_id, redo, style="dashed")
339351
elif powl.operator == Operator.XOR:
340352
with importlib.resources.path(
@@ -351,7 +363,9 @@ def repr_powl(powl, viz, color_map, level):
351363
fixedsize="true",
352364
)
353365
for child in powl.children:
354-
repr_powl(child, block, color_map, level=level + 1)
366+
repr_powl(
367+
child, block, color_map, level=level + 1, base_color=base_color
368+
)
355369
add_operator_edge(block, this_node_id, child)
356370

357371

pm4py/visualization/powl/variants/net.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,12 @@ def apply(
312312

313313
nodes, edges = simplify_and_gateways(nodes, edges)
314314

315-
rankdir = "LR"
316-
bgcolor = constants.DEFAULT_BGCOLOR
315+
rankdir = exec_utils.get_param_value(
316+
Parameters.RANKDIR, parameters, constants.DEFAULT_RANKDIR_GVIZ
317+
)
318+
bgcolor = exec_utils.get_param_value(
319+
Parameters.BGCOLOR, parameters, constants.DEFAULT_BGCOLOR
320+
)
317321

318322
filename = tempfile.NamedTemporaryFile(suffix=".gv")
319323
viz = Digraph(

0 commit comments

Comments
 (0)