9595 background: var(--doc-codebox-border-color);
9696}
9797
98+ tbody tr.phase-details-row {
99+ border-bottom: none;
100+ }
101+
102+ tbody tr.phase-details-row:hover {
103+ background: transparent;
104+ }
105+
106+ tbody tr.phase-details-row details summary {
107+ font-size: 12px;
108+ padding: 4px 0;
109+ }
110+
111+ tbody tr.phase-details-row details[open] summary {
112+ margin-bottom: 4px;
113+ }
114+
98115/* === Chart/Card Section === */
99116.chart {
100117 padding: 20px;
242259 color: #1a1a1a !important;
243260 }
244261
262+ /* Fix metric title visibility in dark mode */
245263 .chart .metric-title {
246264 color: #b3b3b3;
247265 }
@@ -315,7 +333,7 @@ def get_f7fff0_shade_hex(fraction: float) -> str:
315333
316334 # Define RGB for light and dark end
317335 light_color = (247 , 255 , 240 ) # #f7fff0
318- dark_color = (200 , 255 , 150 ) # slightly darker/more saturated green-yellow
336+ dark_color = (200 , 255 , 150 ) # slightly darker/more saturated green-yellow
319337
320338 # Interpolate RGB channels
321339 r = int (light_color [0 ] + (dark_color [0 ] - light_color [0 ]) * fraction )
@@ -325,7 +343,9 @@ def get_f7fff0_shade_hex(fraction: float) -> str:
325343 return f"#{ r :02x} { g :02x} { b :02x} "
326344
327345
328- def get_node_body (name : str , result : str , cpu_time : float , card : int , est : int , result_size : int , extra_info : str ) -> str : # noqa: D103
346+ def get_node_body (
347+ name : str , result : str , cpu_time : float , card : int , est : int , result_size : int , extra_info : str
348+ ) -> str : # noqa: D103
329349 """
330350 Generate the HTML body for a single node in the tree.
331351 """
@@ -389,8 +409,8 @@ def generate_tree_recursive(json_graph: object, cpu_time: float) -> str: # noqa
389409 return node_prefix_html + node_body + children_html + node_suffix_html
390410
391411
392- # For generating the table in the top left.
393- def generate_timing_html (graph_json : object , query_timings : object ) -> object : # noqa: D103
412+ # For generating the table in the top left with expandable phases
413+ def generate_timing_html (graph_json : object , query_timings : object ) -> object :
394414 json_graph = json .loads (graph_json )
395415 gather_timing_information (json_graph , query_timings )
396416 table_head = """
@@ -411,28 +431,73 @@ def generate_timing_html(graph_json: object, query_timings: object) -> object:
411431 all_phases = query_timings .get_phases ()
412432 query_timings .add_node_timing (NodeTiming ("Execution Time (CPU)" , execution_time , None ))
413433 all_phases = ["Execution Time (CPU)" , * all_phases ]
434+
414435 for phase in all_phases :
415436 summarized_phase = query_timings .get_summary_phase_timings (phase )
416437 summarized_phase .calculate_percentage (execution_time )
417438 phase_column = f"<b>{ phase } </b>" if phase == "Execution Time (CPU)" else phase
439+
440+ # Main phase row
418441 table_body += f"""
419442 <tr>
420443 <td>{ phase_column } </td>
421444 <td>{ round (summarized_phase .time , 8 )} </td>
422445 <td>{ str (summarized_phase .percentage * 100 )[:6 ]} %</td>
423446 </tr>
424447"""
448+
449+ # Add expandable details for individual nodes (except for Execution Time)
450+ if phase != "Execution Time (CPU)" :
451+ phase_timings = query_timings .get_phase_timings (phase )
452+ if len (phase_timings ) > 1 : # Only show details if there are multiple nodes
453+ table_body += f"""
454+ <tr class="phase-details-row">
455+ <td colspan="3">
456+ <details>
457+ <summary style="cursor: pointer; padding: 4px 0; color: var(--text-secondary-color);">
458+ Show { len (phase_timings )} nodes
459+ </summary>
460+ <table style="margin: 8px 0; width: 100%; border: none; box-shadow: none;">
461+ <tbody>
462+ """
463+ for node_timing in sorted (phase_timings , key = lambda x : x .time , reverse = True ):
464+ node_timing .calculate_percentage (execution_time )
465+ depth_indent = " " * (node_timing .depth * 4 )
466+ table_body += f"""
467+ <tr style="background: var(--doc-codebox-background-color);">
468+ <td style="padding: 4px 12px; border: none;">{ depth_indent } ↳ Depth { node_timing .depth } </td>
469+ <td style="padding: 4px 12px; border: none;">{ round (node_timing .time , 8 )} </td>
470+ <td style="padding: 4px 12px; border: none;">{ str (node_timing .percentage * 100 )[:6 ]} %</td>
471+ </tr>
472+ """
473+ table_body += """
474+ </tbody>
475+ </table>
476+ </details>
477+ </td>
478+ </tr>
479+ """
480+
425481 table_body += table_end
426482 return table_head + table_body
427483
484+
428485def generate_metric_grid_html (graph_json : str ) -> str : # noqa: D103
429486 json_graph = json .loads (graph_json )
430487 metrics = {
431- "Execution Time (s)" : f"{ float (json_graph .get ("latency" , "N/A" )):.4f} " ,
432- "Total GB Read" : f"{ float (json_graph .get ("total_bytes_read" , "N/A" )) / (1024 ** 3 ):.4f} " if json_graph .get ("total_bytes_read" , "N/A" ) != "N/A" else "N/A" ,
433- "Total GB Written" : f"{ float (json_graph .get ("total_bytes_written" , "N/A" )) / (1024 ** 3 ):.4f} " if json_graph .get ("total_bytes_written" , "N/A" ) != "N/A" else "N/A" ,
434- "Peak Memory (GB)" : f"{ float (json_graph .get ("system_peak_buffer_memory" , "N/A" )) / (1024 ** 3 ):.4f} " if json_graph .get ("system_peak_buffer_memory" , "N/A" ) != "N/A" else "N/A" ,
435- "Rows Scanned" : f"{ json_graph .get ("cumulative_rows_scanned" , "N/A" ):,} " if json_graph .get ("cumulative_rows_scanned" , "N/A" ) != "N/A" else "N/A" ,
488+ "Execution Time (s)" : f"{ float (json_graph .get ('latency' , 'N/A' )):.4f} " ,
489+ "Total GB Read" : f"{ float (json_graph .get ('total_bytes_read' , 'N/A' )) / (1024 ** 3 ):.4f} "
490+ if json_graph .get ("total_bytes_read" , "N/A" ) != "N/A"
491+ else "N/A" ,
492+ "Total GB Written" : f"{ float (json_graph .get ('total_bytes_written' , 'N/A' )) / (1024 ** 3 ):.4f} "
493+ if json_graph .get ("total_bytes_written" , "N/A" ) != "N/A"
494+ else "N/A" ,
495+ "Peak Memory (GB)" : f"{ float (json_graph .get ('system_peak_buffer_memory' , 'N/A' )) / (1024 ** 3 ):.4f} "
496+ if json_graph .get ("system_peak_buffer_memory" , "N/A" ) != "N/A"
497+ else "N/A" ,
498+ "Rows Scanned" : f"{ json_graph .get ('cumulative_rows_scanned' , 'N/A' ):,} "
499+ if json_graph .get ("cumulative_rows_scanned" , "N/A" ) != "N/A"
500+ else "N/A" ,
436501 }
437502 metric_grid_html = """<div class="metrics-grid">"""
438503 for key in metrics .keys ():
@@ -445,6 +510,7 @@ def generate_metric_grid_html(graph_json: str) -> str: # noqa: D103
445510 metric_grid_html += "</div>"
446511 return metric_grid_html
447512
513+
448514def generate_sql_query_html (graph_json : str ) -> str : # noqa: D103
449515 json_graph = json .loads (graph_json )
450516 sql_query = json_graph .get ("query_name" , "N/A" )
@@ -459,6 +525,7 @@ def generate_sql_query_html(graph_json: str) -> str: # noqa: D103
459525 """
460526 return sql_html
461527
528+
462529def generate_tree_html (graph_json : object ) -> str : # noqa: D103
463530 json_graph = json .loads (graph_json )
464531 cpu_time = float (json_graph ["cpu_time" ])
@@ -485,9 +552,7 @@ def generate_ipython(json_input: str) -> str: # noqa: D103
485552
486553def generate_style_html (graph_json : str , include_meta_info : bool ) -> None : # noqa: D103, FBT001
487554 treeflex_css = '<link rel="stylesheet" href="https://unpkg.com/treeflex/dist/css/treeflex.css">\n '
488- libraries = (
489- '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">\n '
490- )
555+ libraries = '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">\n '
491556 return {"treeflex_css" : treeflex_css , "duckdb_css" : qgraph_css , "libraries" : libraries , "chart_script" : "" }
492557
493558
0 commit comments