{# Trapezoid and rect macros #} {% macro trapezoid_entry(node_width, node_height, top_margin=5, r=6, fill=None, stroke=None) -%} {% if fill and fill|trim != '' -%} {%- set gradient_id = "glassGradient_" ~ fill|replace("#", "") -%} {%- else -%} {%- endif -%} {%- endmacro %} {% macro trapezoid_exit(node_width, node_height, bottom_margin=5, r=6, fill=None, stroke=None) -%} {% if fill and fill|trim != '' -%} {%- set gradient_id = "glassGradient_" ~ fill|replace("#", "") -%} {%- else -%} {%- endif -%} {%- endmacro %} {% macro node_rect_box(node_width, node_height, rx=6, ry=6, fill=None, stroke=None) -%} {% if fill and fill|trim != '' -%} {%- set gradient_id = "glassGradient_" ~ fill|replace("#", "") -%} {%- else -%} {%- endif -%} {%- endmacro %} {% macro taskgroup_rect_box(x, y, width, height, cls="", rx=6, ry=6, fill=None, stroke=None, stroke_width=1.5, dash="4,4", opacity=1) -%} {% if fill and fill|trim != '' -%} {%- set gradient_id = "glassGradient_" ~ fill|replace("#", "") -%} {%- else -%} {%- endif -%} {%- endmacro %} {% macro parallel_rect_box(x, y, width, height, rx=6, ry=6, fill=None, stroke="rgb(254, 175, 255)", stroke_width=1.5, opacity=0.2) -%} {% if fill and fill|trim != '' -%} {%- set gradient_id = "parallelGradient_" ~ fill|replace("#", "") -%} {%- else -%} {%- endif -%} {%- endmacro %} {%- set spacing_x = 130 %} {%- set spacing_y = 80 %} {%- set node_width = 114 %} {%- set node_height = 32 %} {%- set tg_margin_x = 15 -%} {%- set tg_label_margin_top = 10 -%} {%- set tg_margin_bottom = 4 -%} {%- set tg_label_height = 15 -%} {%- set parallel_margin_y = 10 -%} {%- set parallel_margin_x = 10 -%} {%- set tooltip_y_offset = -80 -%} {%- set id_to_node = {} %} {%- for node in nodes %} {%- set _ = id_to_node.update({node.uuid: node}) %} {%- endfor %} {%- set id_to_taskgroup = {} %} {%- for tg in taskgroups %} {%- set _ = id_to_taskgroup.update({tg.uuid: tg}) %} {%- endfor %} {#- Determine parallel-depth by Depth-First Search #} {%- set levels = {} %} {%- macro visit(node_id, depth=0) %} {%- if node_id in levels %} {%- set _ = levels[node_id].append(depth) %} {%- else %} {%- set _ = levels.update({node_id: [depth]}) %} {%- endif %} {%- for child in id_to_node[node_id].children %} {{- visit(child, depth + 1) }} {%- endfor %} {%- endmacro %} {{- visit(nodes[0].uuid) }} {%- set depth_to_nodes = {} %} {%- for node_id, dlist in levels.items() %} {%- set depth = dlist|max %} {%- set _ = depth_to_nodes.update({depth: (depth_to_nodes.get(depth, []) + [node_id])}) %} {%- endfor %} {#- Calculate centered positions #} {%- set max_nodes_per_level = 0 %} {%- for depth, ids in depth_to_nodes.items() %} {%- if ids|length > max_nodes_per_level %} {%- set max_nodes_per_level = ids|length %} {%- endif %} {%- endfor %} {#- Calculate total width needed for the widest level #} {%- set total_width = (max_nodes_per_level - 1) * spacing_x %} {#- Calculate starting offset to center the entire DAG #} {%- set svg_width = 800 %} {# Approximate SVG viewport width - varies with browser window size #} {%- set dag_start_x = (svg_width - total_width) / 2 %} {%- if dag_start_x < spacing_x %} {%- set dag_start_x = spacing_x %} {%- endif %} {%- set id_to_pos = {} %} {%- for depth, ids in depth_to_nodes.items() %} {%- set level_width = (ids|length - 1) * spacing_x %} {%- set level_start_x = dag_start_x + (total_width - level_width) / 2 %} {%- for id in ids %} {%- set i = loop.index0 %} {%- set x = level_start_x + i * spacing_x %} {%- set y = -tooltip_y_offset + depth * spacing_y %} {# DAG top margin = 1*-tooltip_y_offset #} {%- set _ = id_to_pos.update({id: {'x': x, 'y': y} }) %} {%- endfor %} {%- endfor %} {#- Determine taskgroup-nesting by Depth-First Search #} {# recursive macro: how deeply is each node wrapped in groups? #} {%- macro nesting(node) %} {%- if node.taskgroup_uuid is not none and node.taskgroup_uuid in id_to_taskgroup %} {{ 1 + (nesting_tg(id_to_taskgroup[node.taskgroup_uuid]) | int) }} {%- else %} 0 {%- endif %} {%- endmacro %} {%- macro nesting_tg(tg) %} {%- set result = {'value': 0} %} {%- for parent_tg in id_to_taskgroup.values() if tg.uuid in parent_tg.elements %} {%- set _ = result.update({'value': 1 + (nesting_tg(parent_tg) | int) }) %} {% endfor %} {{ result.value }} {%- endmacro %} {%- set id_to_nesting = {} %} {%- for node in nodes %} {%- set _ = id_to_nesting.update({ node.uuid: nesting(node) | int }) %} {%- endfor %} {#- Calculate taskgroup bounds and centers #} {%- set taskgroup_bounds = {} %} {%- set taskgroup_nodes = {} %} {%- for tg in taskgroups %} {%- set tg_coords = {'min_x': 999999, 'max_x': -999999, 'min_y': 999999, 'max_y': -999999} %} {%- set found_nodes = {'found_nodes': false} %} {%- set tg_node_list = [] %} {%- for node in nodes %} {%- if node.taskgroup_uuid == tg.uuid %} {%- set _ = found_nodes.update({'found_nodes': true}) %} {%- set _ = tg_node_list.append(node.uuid) %} {%- set pos = id_to_pos[node.uuid] %} {%- if pos.x < tg_coords.min_x %} {%- set _ = tg_coords.update({'min_x': pos.x}) %} {%- endif %} {%- if pos.x + node_width > tg_coords.max_x %} {%- set _ = tg_coords.update({'max_x': pos.x + node_width}) %} {%- endif %} {%- if pos.y < tg_coords.min_y %} {%- set _ = tg_coords.update({'min_y': pos.y}) %} {%- endif %} {%- if pos.y + node_height > tg_coords.max_y %} {%- set _ = tg_coords.update({'max_y': pos.y + node_height}) %} {%- endif %} {%- endif %} {%- endfor %} {%- if found_nodes.found_nodes %} {%- set center_x = tg_coords.min_x + (tg_coords.max_x - tg_coords.min_x)/2 %} {%- set _ = tg_coords.update({'center_x': center_x, 'container_top': tg_coords.min_y - (tg_label_height + tg_label_margin_top), 'container_bottom': tg_coords.max_y + node_height + tg_margin_bottom - (tg_label_height + tg_label_margin_top)}) %} {%- set _ = taskgroup_bounds.update({tg.uuid: tg_coords}) %} {%- set _ = taskgroup_nodes.update({tg.uuid: tg_node_list}) %} {%- endif %} {%- endfor %} {#- --- NEW PREPASS: compute parallel_depth at render time for each node --- #} {%- set parallel_depths = {} %} {%- macro compute_pd(node_id, depth=0) %} {%- set node = id_to_node[node_id] %} {%- if node.is_parallel %} {%- set depth = depth + 1 %} {%- endif %} {%- if node.merge_func is not none and not node.is_parallel %} {%- set depth = depth - 1 %} {%- endif %} {%- set _ = parallel_depths.update({node_id: depth}) %} {%- for child in node.children %} {{- compute_pd(child, depth) }} {%- endfor %} {%- endmacro %} {{- compute_pd(nodes[0].uuid) }} {#- Calculate sub-DAG bounds (parallel regions) #} {%- set parallel_bounds = {} %} {%- set parallel_max_depths = {} %} {%- for node in nodes %} {%- if node.is_parallel %} {# Find the merge node for this parallel region #} {%- set merge_found = {'node': none, 'depth': parallel_depths[node.uuid]} %} {%- macro find_merge(current_id, target_depth) %} {%- set current = id_to_node[current_id] %} {%- if current.merge_func is not none and not current.is_parallel and parallel_depths[current_id] == target_depth - 1 %} {%- set _ = merge_found.update({'node': current}) %} {%- else %} {%- for child in current.children %} {%- if merge_found.node is none %} {{- find_merge(child, target_depth) }} {%- endif %} {%- endfor %} {%- endif %} {%- endmacro %} {{- find_merge(node.uuid, merge_found.depth) }} {# Collect all nodes between parallel and merge (inclusive) #} {%- set parallel_nodes = [node.uuid] %} {%- if merge_found.node %} {%- macro collect_region_nodes(current_id, merge_id, min_depth, collected) %} {%- set current = id_to_node[current_id] %} {%- if current_id != merge_id %} {%- for child in current.children %} {%- if child not in collected and parallel_depths[child] >= min_depth %} {%- set _ = collected.append(child) %} {{- collect_region_nodes(child, merge_id, min_depth, collected) }} {%- endif %} {%- endfor %} {%- endif %} {%- endmacro %} {{- collect_region_nodes(node.uuid, merge_found.node.uuid, merge_found.depth - 1, parallel_nodes) }} {%- set _ = parallel_nodes.append(merge_found.node.uuid) %} {%- endif %} {# Calculate bounds accounting for underlays and taskgroup margins #} {%- if parallel_nodes %} {%- set p_coords = {'min_x': 999999, 'max_x': -999999, 'min_y': 999999, 'max_y': -999999} %} {%- set max_depth = {'value': parallel_depths[node.uuid]} %} {%- for p_node_id in parallel_nodes %} {%- set pos = id_to_pos[p_node_id] %} {%- set node_obj = id_to_node[p_node_id] %} {%- set pd = parallel_depths[p_node_id] %} {%- if pd > max_depth.value %} {%- set _ = max_depth.update({'value': pd}) %} {%- endif %} {%- set underlay_offset = 3 * pd %} {# If node is in a taskgroup, use taskgroup bounds #} {%- if node_obj.taskgroup_uuid and node_obj.taskgroup_uuid in taskgroup_bounds %} {%- set tg_bounds = taskgroup_bounds[node_obj.taskgroup_uuid] %} {%- set tg_underlay_offset = 3.5 * pd %} {%- set effective_min_x = tg_bounds.min_x - tg_margin_x - tg_underlay_offset %} {%- set effective_max_x = tg_bounds.max_x + tg_margin_x + tg_underlay_offset %} {%- set effective_min_y = tg_bounds.min_y - (tg_label_height + tg_label_margin_top) %} {%- set effective_max_y = pos.y + underlay_offset %} {%- else %} {%- set effective_min_x = pos.x - underlay_offset %} {%- set effective_max_x = pos.x + node_width + underlay_offset %} {%- set effective_min_y = pos.y %} {%- set effective_max_y = pos.y + underlay_offset %} {%- endif %} {%- if effective_min_x < p_coords.min_x %} {%- set _ = p_coords.update({'min_x': effective_min_x}) %} {%- endif %} {%- if effective_max_x > p_coords.max_x %} {%- set _ = p_coords.update({'max_x': effective_max_x}) %} {%- endif %} {%- if effective_min_y < p_coords.min_y %} {%- set _ = p_coords.update({'min_y': effective_min_y}) %} {%- endif %} {%- if effective_max_y > p_coords.max_y %} {%- set _ = p_coords.update({'max_y': effective_max_y}) %} {%- endif %} {%- endfor %} {%- set _ = parallel_bounds.update({node.uuid: p_coords}) %} {%- set _ = parallel_max_depths.update({node.uuid: max_depth.value}) %} {%- endif %} {%- endif %} {%- endfor %} {# Draw sub-DAG (parallel) containers #} {%- for node in nodes %} {%- if node.is_parallel and node.uuid in parallel_bounds %} {%- set p_coords = parallel_bounds[node.uuid] %} {%- set level = parallel_depths[node.uuid] %} {%- set max_depth = parallel_max_depths.get(node.uuid, level) %} {%- set level_margin_x = parallel_margin_x * (max_depth - level + 1) %} {{ parallel_rect_box( p_coords.min_x - level_margin_x, p_coords.min_y - parallel_margin_y, p_coords.max_x - p_coords.min_x + 2*level_margin_x, p_coords.max_y - p_coords.min_y + node_height + 2*parallel_margin_y, fill=node.ui_css.background if node.ui_css and node.ui_css.background else None ) }} {%- endif %} {%- endfor %} {#- Draw arrows #} {#- --- ARROW LOGIC FOR TASKGROUPS --- #} {#- To ensure only one outgoing (and one incoming) arrow per taskgroup: #} {%- set tg_outs = {} %} {%- set tg_ins = {} %} {%- set tg_link_pairs = [] %} {%- set parallel_outs = {} %} {%- set parallel_ins = {} %} {%- set parallel_link_pairs = [] %} {%- set normal_arrows = [] %} {# Build map of merge nodes to their parallel regions #} {%- set merge_to_parallel = {} %} {%- for node in nodes %} {%- if node.is_parallel and node.uuid in parallel_bounds %} {# Find the merge for this parallel #} {%- set merge_found = {'node': none, 'depth': parallel_depths[node.uuid]} %} {%- macro find_merge_for_map(current_id, target_depth) %} {%- set current = id_to_node[current_id] %} {%- if current.merge_func is not none and not current.is_parallel and parallel_depths[current_id] == target_depth - 1 %} {%- set _ = merge_found.update({'node': current}) %} {%- else %} {%- for child in current.children %} {%- if merge_found.node is none %} {{- find_merge_for_map(child, target_depth) }} {%- endif %} {%- endfor %} {%- endif %} {%- endmacro %} {{- find_merge_for_map(node.uuid, merge_found.depth) }} {%- if merge_found.node %} {%- set _ = merge_to_parallel.update({merge_found.node.uuid: node.uuid}) %} {%- endif %} {%- endif %} {%- endfor %} {%- for node in nodes %} {%- set src_tg = node.taskgroup_uuid %} {%- set src_parallel = merge_to_parallel.get(node.uuid, none) %} {%- for child_id in node.children %} {%- set child_node = id_to_node[child_id] %} {%- set tgt_tg = child_node.taskgroup_uuid %} {%- set tgt_parallel = none %} {%- if child_node.is_parallel and child_node.uuid in parallel_bounds %} {%- set tgt_parallel = child_node.uuid %} {%- endif %} {%- if src_tg and tgt_tg and src_tg == tgt_tg %} {%- set _ = normal_arrows.append((node.uuid, child_id)) %} {%- elif src_tg and (not tgt_tg or src_tg != tgt_tg) %} {%- set pair = (src_tg, tgt_tg or '__NONE__') %} {%- if pair not in tg_outs %} {%- set _ = tg_outs.update({pair: (node.uuid, child_id)}) %} {%- set _ = tg_link_pairs.append(('out', pair, node.uuid, child_id)) %} {%- endif %} {%- elif tgt_tg and (not src_tg or src_tg != tgt_tg) %} {%- set pair = (src_tg or '__NONE__', tgt_tg) %} {%- if pair not in tg_ins %} {%- set _ = tg_ins.update({pair: (node.uuid, child_id)}) %} {%- set _ = tg_link_pairs.append(('in', pair, node.uuid, child_id)) %} {%- endif %} {%- elif src_parallel and (not tgt_parallel or src_parallel != tgt_parallel) %} {%- set pair = (src_parallel, tgt_parallel or '__NONE__') %} {%- if pair not in parallel_outs %} {%- set _ = parallel_outs.update({pair: (node.uuid, child_id)}) %} {%- set _ = parallel_link_pairs.append(('out', pair, node.uuid, child_id)) %} {%- endif %} {%- elif tgt_parallel and (not src_parallel or src_parallel != tgt_parallel) %} {%- set pair = (src_parallel or '__NONE__', tgt_parallel) %} {%- if pair not in parallel_ins %} {%- set _ = parallel_ins.update({pair: (node.uuid, child_id)}) %} {%- set _ = parallel_link_pairs.append(('in', pair, node.uuid, child_id)) %} {%- endif %} {%- else %} {%- set _ = normal_arrows.append((node.uuid, child_id)) %} {%- endif %} {%- endfor %} {%- endfor %} {#- Draw one arrow for each stored pair for incoming/outgoing tg pairs #} {%- for kind, pair, src, tgt in tg_link_pairs %} {%- set src_tg = pair[0] %} {%- set tgt_tg = pair[1] %} {%- if kind == 'out' and src_tg != '__NONE__' %} {%- set tg_bounds = taskgroup_bounds[src_tg] %} {%- set src_x = tg_bounds.center_x %} {%- set src_y = tg_bounds.container_bottom %} {%- set tgt_x = id_to_pos[tgt].x + node_width/2 %} {%- set tgt_y = id_to_pos[tgt].y %} {%- if tgt_tg != '__NONE__' and tgt_tg in taskgroup_bounds %} {%- set tgt_bounds = taskgroup_bounds[tgt_tg] %} {%- set tgt_x = tgt_bounds.center_x %} {%- set tgt_y = tgt_bounds.container_top %} {%- else %} {%- endif %} {%- elif kind == 'in' and tgt_tg != '__NONE__' %} {%- if src_tg == '__NONE__' %} {%- set tgt_bounds = taskgroup_bounds[tgt_tg] %} {%- set tgt_x = tgt_bounds.center_x %} {%- set tgt_y = tgt_bounds.container_top %} {%- set src_x = id_to_pos[src].x + node_width/2 %} {%- set src_y = id_to_pos[src].y + node_height %} {%- endif %} {%- endif %} {% endfor -%} {#- Draw one arrow for each stored pair for incoming/outgoing parallel pairs #} {%- for kind, pair, src, tgt in parallel_link_pairs %} {%- set src_parallel = pair[0] %} {%- set tgt_parallel = pair[1] %} {%- if kind == 'out' and src_parallel != '__NONE__' %} {%- set p_bounds = parallel_bounds[src_parallel] %} {%- set src_x = (p_bounds.min_x + p_bounds.max_x) / 2 %} {%- set src_y = p_bounds.max_y + node_height + parallel_margin_y %} {%- set tgt_x = id_to_pos[tgt].x + node_width/2 %} {%- set tgt_y = id_to_pos[tgt].y %} {%- if tgt_parallel != '__NONE__' and tgt_parallel in parallel_bounds %} {%- set tgt_p_bounds = parallel_bounds[tgt_parallel] %} {%- set tgt_x = (tgt_p_bounds.min_x + tgt_p_bounds.max_x) / 2 %} {%- set tgt_y = tgt_p_bounds.min_y - parallel_margin_y %} {%- else %} {%- endif %} {%- elif kind == 'in' and tgt_parallel != '__NONE__' %} {%- if src_parallel == '__NONE__' %} {%- set tgt_p_bounds = parallel_bounds[tgt_parallel] %} {%- set tgt_x = (tgt_p_bounds.min_x + tgt_p_bounds.max_x) / 2 %} {%- set tgt_y = tgt_p_bounds.min_y - parallel_margin_y %} {%- set src_x = id_to_pos[src].x + node_width/2 %} {%- set src_y = id_to_pos[src].y + node_height %} {%- endif %} {%- endif %} {% endfor -%} {# Draw all standard arrows (within taskgroups or outside any) #} {%- for src, tgt in normal_arrows %} {%- set src_pos = id_to_pos[src] %} {%- set tgt_pos = id_to_pos[tgt] %} {% endfor %} {# Draw taskgroups containers, per-group parallel-level underlays #} {%- for tg in taskgroups %} {%- set tg_coords = taskgroup_bounds.get(tg.uuid) %} {%- if tg_coords %} {# If this taskgroup contains nodes that have parallel layers, draw one SHIFTED underlay per parallel level for the entire taskgroup. This replaces per-node underlays for nodes that belong to taskgroups. #} {%- set tg_node_list = taskgroup_nodes.get(tg.uuid, []) %} {# Compute max parallel depth for this taskgroup using a dict accumulator #} {%- set max_pd = {'value': 0} %} {%- for n_uuid in tg_node_list %} {%- set pd = parallel_depths.get(n_uuid, 0) %} {%- if pd > max_pd.value %} {%- set _ = max_pd.update({'value': pd}) %} {%- endif %} {%- endfor %} {%- if max_pd.value > 0 %} {# draw layers from outermost to innermost (higher level first) #} {%- for level in range(max_pd.value, 0, -1) %} {%- set offset = 3.5 * level %} {{ taskgroup_rect_box( tg_coords.min_x - tg_margin_x, tg_coords.min_y - (tg_label_height + tg_label_margin_top), tg_coords.max_x - tg_coords.min_x + 2*tg_margin_x, tg_coords.max_y - tg_coords.min_y + node_height + tg_margin_bottom, fill=( tg.ui_css.background if tg.ui_css and tg.ui_css.background else None ), stroke=tg.ui_css.border if tg.ui_css and tg.ui_css.border else None ) }} {%- endfor %} {%- endif %} {# Draw the base container last using the same macro #} {{ taskgroup_rect_box( tg_coords.min_x - tg_margin_x, tg_coords.min_y - (tg_label_height + tg_label_margin_top), tg_coords.max_x - tg_coords.min_x + 2*tg_margin_x, tg_coords.max_y - tg_coords.min_y + node_height + tg_margin_bottom, "taskgroup-container", fill=( tg.ui_css.background if tg.ui_css and tg.ui_css.background else None ), stroke=tg.ui_css.border if tg.ui_css and tg.ui_css.border else None ) }} {{ tg.name }} {%- endif %} {%- endfor %} {# Draw nodes #} {%- set state = {'parallel_depth': 0} %} {%- for node in nodes %} {%- set pos = id_to_pos[node.uuid] %} {# Increase parallel depth BEFORE rendering node if parallel #} {%- if node.is_parallel %} {%- set _ = state.update({'parallel_depth': state.parallel_depth + 1}) %} {%- endif %} {# Decrease parallel depth BEFORE rendering node if merge #} {%- if node.merge_func is not none and not node.is_parallel %} {%- set _ = state.update({'parallel_depth': state.parallel_depth - 1}) %} {%- endif %} {# Render per-node underlays only for nodes NOT part of a taskgroup. If node is in a taskgroup, the underlays were already rendered at the taskgroup level. #} {%- if state.parallel_depth > 0 and not node.taskgroup_uuid %} {%- for level in range(state.parallel_depth, 0, -1) %} {%- set offset = 3 * level %} {%- if node.is_parallel %} {{ trapezoid_entry( node_width, node_height, fill=node.ui_css.background if node.ui_css and node.ui_css.background else None, stroke=node.ui_css.border if node.ui_css and node.ui_css.border else None ) }} {%- elif node.merge_func is not none %} {{ trapezoid_exit( node_width, node_height, fill=node.ui_css.background if node.ui_css and node.ui_css.background else None, stroke=node.ui_css.border if node.ui_css and node.ui_css.border else None ) }} {%- else %} {{ node_rect_box( node_width, node_height, fill=node.ui_css.background if node.ui_css and node.ui_css.background else None, stroke=node.ui_css.border if node.ui_css and node.ui_css.border else None ) }} {%- endif %} {%- endfor %} {%- endif %} {# Render main node shape on top #} {%- if node.is_parallel %} {{ trapezoid_entry( node_width, node_height, fill=node.ui_css.background if node.ui_css and node.ui_css.background else None, stroke=node.ui_css.border if node.ui_css and node.ui_css.border else None ) }} {%- elif node.merge_func is not none %} {{ trapezoid_exit( node_width, node_height, fill=node.ui_css.background if node.ui_css and node.ui_css.background else None, stroke=node.ui_css.border if node.ui_css and node.ui_css.border else None ) }} {%- set merge_text = node.merge_func.name -%} {%- set merge_font = "sans-serif" -%} {%- set merge_font_size = 11 -%} {%- set merge_padding_x = 6 -%} {%- set merge_text_width = get_text_pixel_width(merge_text, merge_font, merge_font_size) -%} {%- set merge_rect_width = merge_text_width + 2 * merge_padding_x -%} {{ merge_text }} {%- else %} {{ node_rect_box( node_width, node_height, fill=node.ui_css.background if node.ui_css and node.ui_css.background else None, stroke=node.ui_css.border if node.ui_css and node.ui_css.border else None ) }} {%- endif %} {{ node.name }} {%- endfor %} {# Draw all tooltips last in a separate group #} {# Node tooltips #} {%- for node in nodes %} {%- if node.docstring %} {%- endif %} {%- endfor %} {# Taskgroup tooltips #} {%- for tg in taskgroups %} {%- if tg.docstring %} {%- set tg_coords = taskgroup_bounds.get(tg.uuid) %} {%- endif %} {%- endfor %}