d6d73e98568b8167198a14b16fffee5329bd89cd
java/com.sap.sailing.windestimation.lab/python/mst_graph_visualizer_graphviz.py
| ... | ... | @@ -7,10 +7,13 @@ large trees. It uses DOT language to create the graph and renders it with graphv |
| 7 | 7 | |
| 8 | 8 | Features: |
| 9 | 9 | - Proper tree layout (top to bottom) |
| 10 | -- Each node shows 4 compartments as an HTML-like table |
|
| 10 | +- Each node shows 4 compartments as an HTML-like table with PORT attributes |
|
| 11 | +- Edges connect to specific compartments |
|
| 11 | 12 | - Best path highlighted in red |
| 13 | +- All other edges shown in green (high prob) or gray (lower prob) |
|
| 12 | 14 | - Edge labels with transition probabilities |
| 13 | -- Color-coded confidence levels |
|
| 15 | +- Color-coded compartments by maneuver type |
|
| 16 | +- Legend explaining colors |
|
| 14 | 17 | |
| 15 | 18 | Usage: |
| 16 | 19 | python mst_graph_visualizer_graphviz.py <input_json_file> [output_file] |
| ... | ... | @@ -39,13 +42,12 @@ TYPE_ORDER = ['TACK', 'JIBE', 'HEAD_UP', 'BEAR_AWAY'] |
| 39 | 42 | TYPE_ABBREV = {'TACK': 'T', 'JIBE': 'J', 'HEAD_UP': 'H', 'BEAR_AWAY': 'B'} |
| 40 | 43 | |
| 41 | 44 | |
| 42 | -def confidence_to_intensity(confidence): |
|
| 43 | - """Convert confidence [0,1] to color intensity for background.""" |
|
| 44 | - # Higher confidence = more saturated color |
|
| 45 | - base_intensity = 40 # Minimum intensity (very light) |
|
| 46 | - max_intensity = 200 # Maximum intensity |
|
| 47 | - intensity = int(base_intensity + confidence * (max_intensity - base_intensity)) |
|
| 48 | - return intensity |
|
| 45 | +def blend_color_with_white(hex_color, factor): |
|
| 46 | + """Blend a hex color with white based on factor (0=white, 1=full color).""" |
|
| 47 | + r = int(int(hex_color[1:3], 16) * factor + 255 * (1 - factor)) |
|
| 48 | + g = int(int(hex_color[3:5], 16) * factor + 255 * (1 - factor)) |
|
| 49 | + b = int(int(hex_color[5:7], 16) * factor + 255 * (1 - factor)) |
|
| 50 | + return f"#{min(255, r):02x}{min(255, g):02x}{min(255, b):02x}" |
|
| 49 | 51 | |
| 50 | 52 | |
| 51 | 53 | def format_wind(comp): |
| ... | ... | @@ -58,8 +60,11 @@ def format_wind(comp): |
| 58 | 60 | return f"{wind_est:.0f}±{wind_width/2:.0f}°" |
| 59 | 61 | |
| 60 | 62 | |
| 61 | -def create_node_label(node, best_type=None): |
|
| 62 | - """Create HTML-like label for a node with 4 compartments.""" |
|
| 63 | +def create_node_label_with_ports(node, best_type=None): |
|
| 64 | + """ |
|
| 65 | + Create HTML-like label for a node with 4 compartments. |
|
| 66 | + Each compartment has a PORT attribute for edge connections. |
|
| 67 | + """ |
|
| 63 | 68 | compartments = {c['type']: c for c in node['compartments']} |
| 64 | 69 | |
| 65 | 70 | # Extract time from timestamp |
| ... | ... | @@ -69,8 +74,8 @@ def create_node_label(node, best_type=None): |
| 69 | 74 | else: |
| 70 | 75 | time_part = timestamp[:8] if len(timestamp) >= 8 else timestamp |
| 71 | 76 | |
| 72 | - # Build HTML table |
|
| 73 | - html = '<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="2">' |
|
| 77 | + # Build HTML table with PORT attributes |
|
| 78 | + html = '<<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">' |
|
| 74 | 79 | html += '<TR>' |
| 75 | 80 | |
| 76 | 81 | for type_name in TYPE_ORDER: |
| ... | ... | @@ -81,46 +86,72 @@ def create_node_label(node, best_type=None): |
| 81 | 86 | confidence = comp['confidence'] |
| 82 | 87 | is_best = (best_type == type_name) |
| 83 | 88 | |
| 84 | - # Calculate background color |
|
| 89 | + # Calculate background color - blend with white based on confidence |
|
| 85 | 90 | base_color = TYPE_COLORS[type_name] |
| 86 | 91 | if is_best: |
| 87 | - # Highlight best with full color and border |
|
| 92 | + # Best type: full saturation with red border |
|
| 88 | 93 | bg_color = base_color |
| 89 | - border = ' BGCOLOR="{}" COLOR="red"'.format(base_color) |
|
| 94 | + border_attr = f' BGCOLOR="{bg_color}" BORDER="3" COLOR="red"' |
|
| 90 | 95 | else: |
| 91 | - # Fade color based on confidence |
|
| 92 | - intensity = confidence_to_intensity(confidence) |
|
| 93 | - # Make it lighter by blending with white |
|
| 94 | - r = int(int(base_color[1:3], 16) * confidence + 255 * (1-confidence)) |
|
| 95 | - g = int(int(base_color[3:5], 16) * confidence + 255 * (1-confidence)) |
|
| 96 | - b = int(int(base_color[5:7], 16) * confidence + 255 * (1-confidence)) |
|
| 97 | - bg_color = f"#{min(255,r):02x}{min(255,g):02x}{min(255,b):02x}" |
|
| 98 | - border = f' BGCOLOR="{bg_color}"' |
|
| 96 | + # Other types: fade based on confidence (min 0.2 to keep some color visible) |
|
| 97 | + blend_factor = max(0.2, confidence) |
|
| 98 | + bg_color = blend_color_with_white(base_color, blend_factor) |
|
| 99 | + border_attr = f' BGCOLOR="{bg_color}"' |
|
| 99 | 100 | |
| 100 | 101 | wind_str = format_wind(comp) |
| 101 | 102 | abbrev = TYPE_ABBREV[type_name] |
| 102 | 103 | |
| 103 | - # Create cell content |
|
| 104 | - cell_content = f'<B>{abbrev}</B><BR/><FONT POINT-SIZE="8">{confidence:.2f}</FONT><BR/><FONT POINT-SIZE="7">{wind_str}</FONT>' |
|
| 104 | + # PORT attribute allows edges to connect to this specific cell |
|
| 105 | + port_name = type_name |
|
| 106 | + |
|
| 107 | + # Create cell content with PORT |
|
| 108 | + cell_content = ( |
|
| 109 | + f'<B>{abbrev}</B><BR/>' |
|
| 110 | + f'<FONT POINT-SIZE="9">{confidence:.2f}</FONT><BR/>' |
|
| 111 | + f'<FONT POINT-SIZE="8">{wind_str}</FONT>' |
|
| 112 | + ) |
|
| 105 | 113 | |
| 106 | - html += f'<TD{border}>{cell_content}</TD>' |
|
| 114 | + html += f'<TD PORT="{port_name}"{border_attr}>{cell_content}</TD>' |
|
| 107 | 115 | |
| 108 | 116 | html += '</TR>' |
| 109 | - html += f'<TR><TD COLSPAN="4"><FONT POINT-SIZE="8">{time_part}</FONT></TD></TR>' |
|
| 117 | + # Footer row with timestamp |
|
| 118 | + html += f'<TR><TD COLSPAN="4" BGCOLOR="white"><FONT POINT-SIZE="9">{time_part}</FONT></TD></TR>' |
|
| 110 | 119 | html += '</TABLE>>' |
| 111 | 120 | |
| 112 | 121 | return html |
| 113 | 122 | |
| 114 | 123 | |
| 115 | -def visualize_mst_graph(data, output_file=None, max_nodes=100, show_low_prob_edges=False): |
|
| 124 | +def create_legend(): |
|
| 125 | + """Create a legend explaining the colors.""" |
|
| 126 | + html = '<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">' |
|
| 127 | + html += '<TR><TD COLSPAN="2" BGCOLOR="white"><B>Legend</B></TD></TR>' |
|
| 128 | + |
|
| 129 | + # Maneuver type colors |
|
| 130 | + for type_name in TYPE_ORDER: |
|
| 131 | + color = TYPE_COLORS[type_name] |
|
| 132 | + abbrev = TYPE_ABBREV[type_name] |
|
| 133 | + html += f'<TR><TD BGCOLOR="{color}">{abbrev}</TD><TD BGCOLOR="white">{type_name}</TD></TR>' |
|
| 134 | + |
|
| 135 | + # Edge colors |
|
| 136 | + html += '<TR><TD COLSPAN="2" BGCOLOR="white"><B>Edges</B></TD></TR>' |
|
| 137 | + html += '<TR><TD BGCOLOR="white"><FONT COLOR="red">━━</FONT></TD><TD BGCOLOR="white">Best Path</TD></TR>' |
|
| 138 | + html += '<TR><TD BGCOLOR="white"><FONT COLOR="darkgreen">━━</FONT></TD><TD BGCOLOR="white">High Prob (>1%)</TD></TR>' |
|
| 139 | + html += '<TR><TD BGCOLOR="white"><FONT COLOR="gray50">━━</FONT></TD><TD BGCOLOR="white">Medium Prob</TD></TR>' |
|
| 140 | + html += '<TR><TD BGCOLOR="white"><FONT COLOR="gray80">━━</FONT></TD><TD BGCOLOR="white">Low Prob</TD></TR>' |
|
| 141 | + |
|
| 142 | + html += '</TABLE>>' |
|
| 143 | + return html |
|
| 144 | + |
|
| 145 | + |
|
| 146 | +def visualize_mst_graph(data, output_file=None, max_nodes=100, min_edge_prob=0.0): |
|
| 116 | 147 | """ |
| 117 | - Create graphviz visualization of MST graph. |
|
| 148 | + Create graphviz visualization of MST graph with edges connecting to specific compartments. |
|
| 118 | 149 | |
| 119 | 150 | Args: |
| 120 | 151 | data: Parsed JSON data from MstGraphExporter |
| 121 | 152 | output_file: Output file path (without extension; .pdf/.png will be added) |
| 122 | - max_nodes: Maximum nodes to show |
|
| 123 | - show_low_prob_edges: Whether to show edges with very low probability |
|
| 153 | + max_nodes: Maximum number of nodes to show |
|
| 154 | + min_edge_prob: Minimum edge probability to display (0 = show all) |
|
| 124 | 155 | """ |
| 125 | 156 | nodes = {n['id']: n for n in data['nodes']} |
| 126 | 157 | edges = data['edges'] |
| ... | ... | @@ -130,28 +161,26 @@ def visualize_mst_graph(data, output_file=None, max_nodes=100, show_low_prob_edg |
| 130 | 161 | dot = Digraph(comment='MST Maneuver Graph') |
| 131 | 162 | dot.attr(rankdir='TB') # Top to bottom |
| 132 | 163 | dot.attr('node', shape='plaintext') # Use HTML labels |
| 164 | + dot.attr(splines='polyline') # Use polyline for clearer edge routing |
|
| 165 | + dot.attr(nodesep='0.5') # Horizontal spacing between nodes |
|
| 166 | + dot.attr(ranksep='1.0') # Vertical spacing between ranks |
|
| 133 | 167 | |
| 134 | 168 | # Limit nodes |
| 135 | 169 | nodes_to_draw = list(nodes.keys())[:max_nodes] |
| 136 | 170 | nodes_set = set(nodes_to_draw) |
| 137 | 171 | |
| 172 | + # Add legend |
|
| 173 | + dot.node('legend', create_legend()) |
|
| 174 | + dot.node('legend_spacer', '', shape='none', width='0', height='0') |
|
| 175 | + |
|
| 138 | 176 | # Add nodes |
| 139 | 177 | for node_id in nodes_to_draw: |
| 140 | 178 | node = nodes[node_id] |
| 141 | 179 | best_type = best_paths.get(str(node_id)) |
| 142 | - label = create_node_label(node, best_type) |
|
| 180 | + label = create_node_label_with_ports(node, best_type) |
|
| 143 | 181 | dot.node(str(node_id), label) |
| 144 | 182 | |
| 145 | - # Collect best path edges for highlighting |
|
| 146 | - best_edge_keys = set() |
|
| 147 | - for edge in edges: |
|
| 148 | - if edge.get('isBestPath', False): |
|
| 149 | - key = (edge['from'], edge['to'], edge['fromType'], edge['toType']) |
|
| 150 | - best_edge_keys.add(key) |
|
| 151 | - |
|
| 152 | - # Group edges by (from_node, to_node) for simplicity |
|
| 153 | - # Only show best path edges and edges between same types |
|
| 154 | - edge_groups = {} |
|
| 183 | + # Process ALL edges |
|
| 155 | 184 | for edge in edges: |
| 156 | 185 | from_id = edge['from'] |
| 157 | 186 | to_id = edge['to'] |
| ... | ... | @@ -161,61 +190,55 @@ def visualize_mst_graph(data, output_file=None, max_nodes=100, show_low_prob_edg |
| 161 | 190 | |
| 162 | 191 | is_best = edge.get('isBestPath', False) |
| 163 | 192 | trans_prob = edge['transitionProbability'] |
| 193 | + from_type = edge['fromType'] |
|
| 194 | + to_type = edge['toType'] |
|
| 164 | 195 | |
| 165 | - # Filter: show best path edges, same-type edges, or high probability edges |
|
| 166 | - same_type = edge['fromType'] == edge['toType'] |
|
| 167 | - high_prob = trans_prob > 0.001 |
|
| 168 | - |
|
| 169 | - if is_best or (same_type and high_prob) or show_low_prob_edges: |
|
| 170 | - key = (from_id, to_id) |
|
| 171 | - if key not in edge_groups: |
|
| 172 | - edge_groups[key] = [] |
|
| 173 | - edge_groups[key].append(edge) |
|
| 174 | - |
|
| 175 | - # Add edges (simplified - one edge per node pair, prioritizing best path) |
|
| 176 | - for (from_id, to_id), edge_list in edge_groups.items(): |
|
| 177 | - # Find best edge in this group |
|
| 178 | - best_edge = None |
|
| 179 | - for e in edge_list: |
|
| 180 | - if e.get('isBestPath', False): |
|
| 181 | - best_edge = e |
|
| 182 | - break |
|
| 183 | - |
|
| 184 | - if best_edge is None: |
|
| 185 | - # Use edge with highest probability |
|
| 186 | - best_edge = max(edge_list, key=lambda x: x['transitionProbability']) |
|
| 187 | - |
|
| 188 | - is_best = best_edge.get('isBestPath', False) |
|
| 189 | - trans_prob = best_edge['transitionProbability'] |
|
| 190 | - from_type = best_edge['fromType'] |
|
| 191 | - to_type = best_edge['toType'] |
|
| 196 | + # Skip very low probability edges unless they're best path |
|
| 197 | + if trans_prob < min_edge_prob and not is_best: |
|
| 198 | + continue |
|
| 192 | 199 | |
| 193 | - # Style based on best path |
|
| 200 | + # Style based on best path and probability |
|
| 194 | 201 | if is_best: |
| 195 | 202 | color = 'red' |
| 196 | 203 | penwidth = '2.5' |
| 197 | 204 | style = 'bold' |
| 198 | - else: |
|
| 199 | - # Color based on probability |
|
| 200 | - if trans_prob > 0.01: |
|
| 201 | - color = 'darkgreen' |
|
| 202 | - elif trans_prob > 0.001: |
|
| 203 | - color = 'gray40' |
|
| 204 | - else: |
|
| 205 | - color = 'gray80' |
|
| 206 | - penwidth = '1.0' |
|
| 205 | + fontcolor = 'red' |
|
| 206 | + elif trans_prob > 0.01: |
|
| 207 | + color = 'darkgreen' |
|
| 208 | + penwidth = '1.2' |
|
| 209 | + style = 'solid' |
|
| 210 | + fontcolor = 'darkgreen' |
|
| 211 | + elif trans_prob > 0.001: |
|
| 212 | + color = 'gray50' |
|
| 213 | + penwidth = '0.8' |
|
| 207 | 214 | style = 'solid' |
| 215 | + fontcolor = 'gray50' |
|
| 216 | + else: |
|
| 217 | + color = 'gray80' |
|
| 218 | + penwidth = '0.5' |
|
| 219 | + style = 'dashed' |
|
| 220 | + fontcolor = 'gray70' |
|
| 221 | + |
|
| 222 | + # Format probability label |
|
| 223 | + if trans_prob >= 0.01: |
|
| 224 | + prob_label = f'{trans_prob:.2f}' |
|
| 225 | + elif trans_prob >= 0.001: |
|
| 226 | + prob_label = f'{trans_prob:.3f}' |
|
| 227 | + else: |
|
| 228 | + prob_label = f'{trans_prob:.1e}' |
|
| 208 | 229 | |
| 209 | - # Label shows type transition and probability |
|
| 210 | - label = f'{TYPE_ABBREV[from_type]}→{TYPE_ABBREV[to_type]}\\n{trans_prob:.1e}' |
|
| 230 | + # Use PORT to connect to specific compartments |
|
| 231 | + from_port = f'{from_id}:{from_type}:s' # :s = south (bottom) of cell |
|
| 232 | + to_port = f'{to_id}:{to_type}:n' # :n = north (top) of cell |
|
| 211 | 233 | |
| 212 | - dot.edge(str(from_id), str(to_id), |
|
| 213 | - label=label, |
|
| 214 | - color=color, |
|
| 234 | + dot.edge(from_port, to_port, |
|
| 235 | + label=prob_label, |
|
| 236 | + color=color, |
|
| 215 | 237 | penwidth=penwidth, |
| 216 | 238 | style=style, |
| 217 | - fontsize='8', |
|
| 218 | - fontcolor=color) |
|
| 239 | + fontsize='7', |
|
| 240 | + fontcolor=fontcolor, |
|
| 241 | + arrowsize='0.6') |
|
| 219 | 242 | |
| 220 | 243 | # Render |
| 221 | 244 | if output_file: |
| ... | ... | @@ -235,10 +258,10 @@ def visualize_mst_graph(data, output_file=None, max_nodes=100, show_low_prob_edg |
| 235 | 258 | return dot |
| 236 | 259 | |
| 237 | 260 | |
| 238 | -def create_detailed_edge_graph(data, output_file=None, max_depth=10): |
|
| 261 | +def create_detailed_edge_graph(data, output_file=None, max_depth=10, min_edge_prob=0.0): |
|
| 239 | 262 | """ |
| 240 | 263 | Create a more detailed graph showing all compartment-to-compartment edges. |
| 241 | - Each node compartment becomes its own graphviz node. |
|
| 264 | + Each node compartment becomes its own graphviz node, grouped by cluster. |
|
| 242 | 265 | |
| 243 | 266 | This is useful for small subtrees to see the full inner graph structure. |
| 244 | 267 | """ |
| ... | ... | @@ -252,43 +275,62 @@ def create_detailed_edge_graph(data, output_file=None, max_depth=10): |
| 252 | 275 | |
| 253 | 276 | dot = Digraph(comment='MST Detailed Inner Graph') |
| 254 | 277 | dot.attr(rankdir='TB') |
| 278 | + dot.attr(nodesep='0.3') |
|
| 279 | + dot.attr(ranksep='0.8') |
|
| 280 | + |
|
| 281 | + # Add legend |
|
| 282 | + dot.node('legend', create_legend()) |
|
| 255 | 283 | |
| 256 | 284 | # Create subgraph for each tree node (to keep compartments together) |
| 257 | 285 | for node in nodes_to_draw: |
| 258 | 286 | node_id = node['id'] |
| 259 | 287 | best_type = best_paths.get(str(node_id)) |
| 260 | 288 | |
| 289 | + # Extract time from timestamp for label |
|
| 290 | + timestamp = node.get('timestamp', '') |
|
| 291 | + if ' ' in timestamp: |
|
| 292 | + time_part = timestamp.split(' ')[1][:8] |
|
| 293 | + else: |
|
| 294 | + time_part = str(node_id) |
|
| 295 | + |
|
| 261 | 296 | with dot.subgraph(name=f'cluster_{node_id}') as c: |
| 262 | - c.attr(label=f"Node {node_id}") |
|
| 263 | - c.attr(style='rounded') |
|
| 297 | + c.attr(label=f'{time_part}') |
|
| 298 | + c.attr(style='rounded,filled') |
|
| 299 | + c.attr(fillcolor='white') |
|
| 264 | 300 | |
| 265 | 301 | for comp in node['compartments']: |
| 266 | 302 | type_name = comp['type'] |
| 267 | 303 | comp_id = f"{node_id}_{type_name}" |
| 268 | 304 | |
| 269 | 305 | is_best = (best_type == type_name) |
| 270 | - color = TYPE_COLORS[type_name] |
|
| 306 | + base_color = TYPE_COLORS[type_name] |
|
| 307 | + confidence = comp['confidence'] |
|
| 271 | 308 | |
| 272 | 309 | if is_best: |
| 273 | - style = 'filled,bold' |
|
| 274 | - fillcolor = color |
|
| 310 | + fillcolor = base_color |
|
| 275 | 311 | fontcolor = 'white' |
| 312 | + penwidth = '3' |
|
| 313 | + pencolor = 'red' |
|
| 276 | 314 | else: |
| 277 | - style = 'filled' |
|
| 278 | - # Lighter version |
|
| 279 | - fillcolor = f"{color}40" # 40 = 25% opacity in hex |
|
| 315 | + # Blend based on confidence |
|
| 316 | + blend_factor = max(0.3, confidence) |
|
| 317 | + fillcolor = blend_color_with_white(base_color, blend_factor) |
|
| 280 | 318 | fontcolor = 'black' |
| 319 | + penwidth = '1' |
|
| 320 | + pencolor = 'black' |
|
| 281 | 321 | |
| 282 | - label = f"{TYPE_ABBREV[type_name]}\\n{comp['confidence']:.2f}\\n{format_wind(comp)}" |
|
| 322 | + label = f"{TYPE_ABBREV[type_name]}\\n{confidence:.2f}\\n{format_wind(comp)}" |
|
| 283 | 323 | |
| 284 | 324 | c.node(comp_id, label, |
| 285 | 325 | shape='box', |
| 286 | - style=style, |
|
| 326 | + style='filled', |
|
| 287 | 327 | fillcolor=fillcolor, |
| 288 | 328 | fontcolor=fontcolor, |
| 289 | - fontsize='10') |
|
| 329 | + fontsize='9', |
|
| 330 | + penwidth=penwidth, |
|
| 331 | + color=pencolor) |
|
| 290 | 332 | |
| 291 | - # Add edges between compartments |
|
| 333 | + # Add ALL edges between compartments |
|
| 292 | 334 | for edge in edges: |
| 293 | 335 | from_id = edge['from'] |
| 294 | 336 | to_id = edge['to'] |
| ... | ... | @@ -302,31 +344,42 @@ def create_detailed_edge_graph(data, output_file=None, max_depth=10): |
| 302 | 344 | is_best = edge.get('isBestPath', False) |
| 303 | 345 | trans_prob = edge['transitionProbability'] |
| 304 | 346 | |
| 305 | - # Only show significant edges |
|
| 306 | - if trans_prob < 0.0001 and not is_best: |
|
| 347 | + # Skip very low probability edges unless best path |
|
| 348 | + if trans_prob < min_edge_prob and not is_best: |
|
| 307 | 349 | continue |
| 308 | 350 | |
| 309 | 351 | if is_best: |
| 310 | 352 | color = 'red' |
| 311 | - penwidth = '2.0' |
|
| 353 | + penwidth = '2.5' |
|
| 354 | + fontcolor = 'red' |
|
| 355 | + elif trans_prob > 0.01: |
|
| 356 | + color = 'darkgreen' |
|
| 357 | + penwidth = '1.2' |
|
| 358 | + fontcolor = 'darkgreen' |
|
| 359 | + elif trans_prob > 0.001: |
|
| 360 | + color = 'gray50' |
|
| 361 | + penwidth = '0.8' |
|
| 362 | + fontcolor = 'gray50' |
|
| 363 | + else: |
|
| 364 | + color = 'gray80' |
|
| 365 | + penwidth = '0.4' |
|
| 366 | + fontcolor = 'gray70' |
|
| 367 | + |
|
| 368 | + # Format probability label |
|
| 369 | + if trans_prob >= 0.01: |
|
| 370 | + prob_label = f'{trans_prob:.2f}' |
|
| 371 | + elif trans_prob >= 0.001: |
|
| 372 | + prob_label = f'{trans_prob:.3f}' |
|
| 312 | 373 | else: |
| 313 | - # Color by probability |
|
| 314 | - if trans_prob > 0.01: |
|
| 315 | - color = 'darkgreen' |
|
| 316 | - penwidth = '1.5' |
|
| 317 | - elif trans_prob > 0.001: |
|
| 318 | - color = 'gray50' |
|
| 319 | - penwidth = '1.0' |
|
| 320 | - else: |
|
| 321 | - color = 'gray80' |
|
| 322 | - penwidth = '0.5' |
|
| 374 | + prob_label = f'{trans_prob:.1e}' |
|
| 323 | 375 | |
| 324 | 376 | dot.edge(from_comp_id, to_comp_id, |
| 325 | - label=f'{trans_prob:.1e}', |
|
| 377 | + label=prob_label, |
|
| 326 | 378 | color=color, |
| 327 | 379 | penwidth=penwidth, |
| 328 | 380 | fontsize='7', |
| 329 | - fontcolor=color) |
|
| 381 | + fontcolor=fontcolor, |
|
| 382 | + arrowsize='0.5') |
|
| 330 | 383 | |
| 331 | 384 | if output_file: |
| 332 | 385 | if '.' in output_file: |
| ... | ... | @@ -342,16 +395,40 @@ def create_detailed_edge_graph(data, output_file=None, max_depth=10): |
| 342 | 395 | |
| 343 | 396 | def main(): |
| 344 | 397 | if len(sys.argv) < 2: |
| 345 | - print("Usage: python mst_graph_visualizer_graphviz.py <input_json_file> [output_file] [--detailed]") |
|
| 398 | + print("Usage: python mst_graph_visualizer_graphviz.py <input_json_file> [output_file] [options]") |
|
| 346 | 399 | print("\nOptions:") |
| 347 | - print(" input_json_file - JSON file exported from MstGraphExporter") |
|
| 348 | - print(" output_file - Output file (extension determines format: .pdf, .png, .svg)") |
|
| 349 | - print(" --detailed - Create detailed compartment-level graph (for small graphs)") |
|
| 400 | + print(" input_json_file - JSON file exported from MstGraphExporter") |
|
| 401 | + print(" output_file - Output file (extension determines format: .pdf, .png, .svg)") |
|
| 402 | + print(" --detailed - Create detailed compartment-level graph (for small graphs)") |
|
| 403 | + print(" --min-prob=<value> - Minimum edge probability to show (default: 0 = show all)") |
|
| 404 | + print(" --max-nodes=<n> - Maximum number of nodes to display (default: 100)") |
|
| 405 | + print("\nCompartment Colors:") |
|
| 406 | + print(" T (TACK) - Green") |
|
| 407 | + print(" J (JIBE) - Blue") |
|
| 408 | + print(" H (HEAD_UP) - Orange") |
|
| 409 | + print(" B (BEAR_AWAY) - Purple") |
|
| 410 | + print("\nEdge Colors:") |
|
| 411 | + print(" Red - Best path (selected by algorithm)") |
|
| 412 | + print(" Dark Green - High probability (>1%)") |
|
| 413 | + print(" Gray - Medium/low probability") |
|
| 350 | 414 | sys.exit(1) |
| 351 | 415 | |
| 352 | 416 | input_file = sys.argv[1] |
| 353 | - output_file = sys.argv[2] if len(sys.argv) > 2 and not sys.argv[2].startswith('--') else None |
|
| 354 | - detailed = '--detailed' in sys.argv |
|
| 417 | + output_file = None |
|
| 418 | + detailed = False |
|
| 419 | + min_prob = 0.0 |
|
| 420 | + max_nodes = 100 |
|
| 421 | + |
|
| 422 | + # Parse arguments |
|
| 423 | + for arg in sys.argv[2:]: |
|
| 424 | + if arg == '--detailed': |
|
| 425 | + detailed = True |
|
| 426 | + elif arg.startswith('--min-prob='): |
|
| 427 | + min_prob = float(arg.split('=')[1]) |
|
| 428 | + elif arg.startswith('--max-nodes='): |
|
| 429 | + max_nodes = int(arg.split('=')[1]) |
|
| 430 | + elif not arg.startswith('--'): |
|
| 431 | + output_file = arg |
|
| 355 | 432 | |
| 356 | 433 | print(f"Loading graph from {input_file}...") |
| 357 | 434 | with open(input_file, 'r') as f: |
| ... | ... | @@ -360,13 +437,14 @@ def main(): |
| 360 | 437 | num_nodes = len(data['nodes']) |
| 361 | 438 | num_edges = len(data['edges']) |
| 362 | 439 | print(f"Loaded {num_nodes} nodes, {num_edges} edges") |
| 440 | + print(f"Minimum edge probability: {min_prob}") |
|
| 363 | 441 | |
| 364 | 442 | if detailed: |
| 365 | 443 | print("Creating detailed compartment-level visualization...") |
| 366 | - create_detailed_edge_graph(data, output_file, max_depth=10) |
|
| 444 | + create_detailed_edge_graph(data, output_file, max_depth=10, min_edge_prob=min_prob) |
|
| 367 | 445 | else: |
| 368 | - print("Creating tree visualization...") |
|
| 369 | - visualize_mst_graph(data, output_file, max_nodes=100) |
|
| 446 | + print(f"Creating tree visualization (max {max_nodes} nodes)...") |
|
| 447 | + visualize_mst_graph(data, output_file, max_nodes=max_nodes, min_edge_prob=min_prob) |
|
| 370 | 448 | |
| 371 | 449 | |
| 372 | 450 | if __name__ == '__main__': |