@@ -211,6 +211,91 @@ def is_grid(specs: list[SubplotSpecDict] | Grid[SubplotSpecDict]) -> bool:
211211 return list_count == len (specs ) and list_count > 0
212212
213213
214+ def extract_title_from_figure (fig : Figure | DeephavenFigure ) -> str | None :
215+ """Extract the title from a figure if it exists
216+
217+ Args:
218+ fig: The figure to extract the title from
219+
220+ Returns:
221+ The title string if it exists, None otherwise
222+
223+ """
224+ if isinstance (fig , DeephavenFigure ):
225+ plotly_fig = fig .get_plotly_fig ()
226+ if plotly_fig is None :
227+ return None
228+ fig = plotly_fig
229+
230+ layout = fig .to_dict ().get ("layout" , {})
231+ title = layout .get ("title" )
232+
233+ if title is None :
234+ return None
235+
236+ # Title can be either a string or a dict with a 'text' key
237+ if isinstance (title , dict ):
238+ return title .get ("text" )
239+ return str (title )
240+
241+
242+ def create_subplot_annotations (
243+ titles : list [str ],
244+ col_starts : list [float ],
245+ col_ends : list [float ],
246+ row_starts : list [float ],
247+ row_ends : list [float ],
248+ rows : int ,
249+ cols : int ,
250+ ) -> list [dict ]:
251+ """Create annotations for subplot titles
252+
253+ Args:
254+ titles: List of titles for each subplot
255+ col_starts: List of column start positions
256+ col_ends: List of column end positions
257+ row_starts: List of row start positions
258+ row_ends: List of row end positions
259+ rows: Number of rows
260+ cols: Number of columns
261+
262+ Returns:
263+ List of annotation dictionaries for plotly
264+
265+ """
266+ annotations = []
267+
268+ for idx , title in enumerate (titles ):
269+ if not title : # Skip empty titles
270+ continue
271+
272+ # Calculate row and col from index (row-major order, but reversed since grid is reversed)
273+ row = idx // cols
274+ col = idx % cols
275+
276+ # Calculate x position (center of column)
277+ x = (col_starts [col ] + col_ends [col ]) / 2
278+
279+ # Calculate y position (top of row with small offset)
280+ y = row_ends [row ]
281+
282+ annotation = {
283+ "text" : title ,
284+ "showarrow" : False ,
285+ "xref" : "paper" ,
286+ "yref" : "paper" ,
287+ "x" : x ,
288+ "y" : y ,
289+ "xanchor" : "center" ,
290+ "yanchor" : "bottom" ,
291+ "font" : {"size" : 16 }
292+ }
293+
294+ annotations .append (annotation )
295+
296+ return annotations
297+
298+
214299def atomic_make_subplots (
215300 * figs : Figure | DeephavenFigure ,
216301 rows : int = 0 ,
@@ -223,6 +308,9 @@ def atomic_make_subplots(
223308 column_widths : list [float ] | None = None ,
224309 row_heights : list [float ] | None = None ,
225310 specs : list [SubplotSpecDict ] | Grid [SubplotSpecDict ] | None = None ,
311+ subplot_titles : list [str ] | tuple [str , ...] | None = None ,
312+ use_existing_titles : bool = False ,
313+ title : str | None = None ,
226314 unsafe_update_figure : Callable = default_callback ,
227315) -> DeephavenFigure :
228316 """Create subplots. Either figs and at least one of rows and cols or grid
@@ -240,6 +328,9 @@ def atomic_make_subplots(
240328 column_widths: See make_subplots
241329 row_heights: See make_subplots
242330 specs: See make_subplots
331+ subplot_titles: See make_subplots
332+ use_existing_titles: See make_subplots
333+ title: See make_subplots
243334
244335 Returns:
245336 DeephavenFigure: The DeephavenFigure with subplots
@@ -287,6 +378,56 @@ def atomic_make_subplots(
287378 col_starts , col_ends = get_domains (column_widths , horizontal_spacing )
288379 row_starts , row_ends = get_domains (row_heights , vertical_spacing )
289380
381+ # Handle subplot titles
382+ final_subplot_titles : list [str ] = []
383+
384+ if use_existing_titles and subplot_titles is not None :
385+ raise ValueError ("Cannot use both use_existing_titles and subplot_titles" )
386+
387+ if use_existing_titles :
388+ # Extract titles from existing figures
389+ for fig_row in grid :
390+ for fig in fig_row :
391+ if fig is None :
392+ final_subplot_titles .append ("" )
393+ else :
394+ extracted_title = extract_title_from_figure (fig )
395+ final_subplot_titles .append (extracted_title if extracted_title else "" )
396+ elif subplot_titles is not None :
397+ # Convert to list if tuple
398+ final_subplot_titles = list (subplot_titles )
399+
400+ # Pad with empty strings if needed
401+ total_subplots = rows * cols
402+ if len (final_subplot_titles ) < total_subplots :
403+ final_subplot_titles .extend (["" ] * (total_subplots - len (final_subplot_titles )))
404+
405+ # Create the custom update function to add annotations and title
406+ def custom_update_figure (fig : Figure ) -> Figure :
407+ # Add subplot title annotations if any
408+ if final_subplot_titles :
409+ annotations = create_subplot_annotations (
410+ final_subplot_titles ,
411+ col_starts ,
412+ col_ends ,
413+ row_starts ,
414+ row_ends ,
415+ rows ,
416+ cols ,
417+ )
418+
419+ # Get existing annotations if any
420+ existing_annotations = list (fig .layout .annotations ) if fig .layout .annotations else []
421+ fig .update_layout (annotations = existing_annotations + annotations )
422+
423+ # Add overall title if provided
424+ if title :
425+ fig .update_layout (title = title )
426+
427+ # Apply user's unsafe_update_figure if provided
428+ result = unsafe_update_figure (fig )
429+ return result if result is not None else fig
430+
290431 return atomic_layer (
291432 * [fig for fig_row in grid for fig in fig_row ],
292433 specs = get_new_specs (
@@ -298,7 +439,7 @@ def atomic_make_subplots(
298439 shared_xaxes ,
299440 shared_yaxes ,
300441 ),
301- unsafe_update_figure = unsafe_update_figure ,
442+ unsafe_update_figure = custom_update_figure ,
302443 # remove the legend title as it is likely incorrect
303444 remove_legend_title = True ,
304445 )
@@ -347,6 +488,9 @@ def make_subplots(
347488 column_widths : list [float ] | None = None ,
348489 row_heights : list [float ] | None = None ,
349490 specs : list [SubplotSpecDict ] | Grid [SubplotSpecDict ] | None = None ,
491+ subplot_titles : list [str ] | tuple [str , ...] | None = None ,
492+ use_existing_titles : bool = False ,
493+ title : str | None = None ,
350494 unsafe_update_figure : Callable = default_callback ,
351495) -> DeephavenFigure :
352496 """Create subplots. Either figs and at least one of rows and cols or grid
@@ -383,6 +527,15 @@ def make_subplots(
383527 'b' is a float that adds bottom padding
384528 'rowspan' is an int to make this figure span multiple rows
385529 'colspan' is an int to make this figure span multiple columns
530+ subplot_titles: (Default value = None)
531+ A list or tuple of titles for each subplot in row-major order.
532+ Empty strings ("") can be included in the list if no subplot title
533+ is desired in that space. Cannot be used with use_existing_titles.
534+ use_existing_titles: (Default value = False)
535+ If True, automatically extracts and uses titles from the input figures
536+ as subplot titles. Cannot be used with subplot_titles.
537+ title: (Default value = None)
538+ The overall title for the combined subplot figure.
386539 unsafe_update_figure: An update function that takes a plotly figure
387540 as an argument and optionally returns a plotly figure. If a figure is not
388541 returned, the plotly figure passed will be assumed to be the return value.
@@ -410,6 +563,10 @@ def make_subplots(
410563 column_widths = column_widths ,
411564 row_heights = row_heights ,
412565 specs = specs ,
566+ subplot_titles = subplot_titles ,
567+ use_existing_titles = use_existing_titles ,
568+ title = title ,
569+ unsafe_update_figure = unsafe_update_figure ,
413570 )
414571
415572 exec_ctx = make_user_exec_ctx ()
0 commit comments