Issue with Iteratively Drawing Whisker Plot in ROOT - Only Last Iteration Displayed


Hello,

I am trying to plot a kind of “whisker” plot in ROOT using TLine for the vertical and horizontal whiskers, and TMarker for the median. The plot is meant to display several “whiskers” iteratively for different datasets. However, I am facing an issue where the final canvas only shows the result of the last iteration. I have tried using canvas.Modified() and canvas.Update() after each drawing step, but the issue persists.

I have attached a screenshot showing the final plot where only the last whisker appears. I would appreciate any guidance on what might be going wrong and how to properly draw multiple whiskers on the same canvas.

Here’s a simplified version of the code I am using:

import ROOT
import numpy as np
import re

def create_stats_plot(data, title="Statistical Boxplot", output_file="boxplot_stats.png", y_scale_factor=1.0):
    """
    Create a boxplot-like visualization with vertical lines and whiskers for two datasets using ROOT.

    Parameters:
    data (dict): A dictionary where keys are folder names, and values are dictionaries containing statistics (min, q1, median, q3, max, etc.).
    title (str): Title of the boxplot.
    output_file (str): The file path to save the output plot.
    y_scale_factor (float): Factor to scale the y-axis.
    """

    # Extract x-axis values (e.g., time or categories) and statistics for both DatasetA and DatasetB
    x_axisA = []
    datasetA_stats = []
    datasetB_stats = []

    #==============================================================================
    #            EXTRACT STATISTICAL DATA FROM THE DICTIONARY
    for folder, stats in data.items():
        match = re.search(r'Exp(\d+)', folder)
        if match:
            x_value = int(match.group(1))
            x_axisA.append(x_value)

            # DatasetA statistics
            datasetA_stats.append([
                stats['datasetA']['min'] / y_scale_factor,    # Min
                stats['datasetA']['q1'] / y_scale_factor,     # Q1
                stats['datasetA']['median'] / y_scale_factor, # Median
                stats['datasetA']['q3'] / y_scale_factor,     # Q3
                stats['datasetA']['max'] / y_scale_factor     # Max
            ])

            # DatasetB statistics
            datasetB_stats.append([
                stats['datasetB']['min'] / y_scale_factor,    # Min
                stats['datasetB']['q1'] / y_scale_factor,     # Q1
                stats['datasetB']['median'] / y_scale_factor, # Median
                stats['datasetB']['q3'] / y_scale_factor,     # Q3
                stats['datasetB']['max'] / y_scale_factor     # Max
            ])

    # Sort the data by x-axis value (e.g., exposure time)
    sorted_indices = np.argsort(x_axisA)
    x_axisA = np.array(x_axisA)[sorted_indices]
    datasetA_stats = np.array(datasetA_stats)[sorted_indices]
    datasetB_stats = np.array(datasetB_stats)[sorted_indices]
    #==============================================================================

    # Create a ROOT TCanvas for drawing
    canvas = ROOT.TCanvas("canvas", title, 800, 600)

    # Set axis labels, title, and grid
    frame = canvas.DrawFrame(np.min(x_axisA) - 1, 
                             np.min([datasetA_stats.min(), datasetB_stats.min()]) - 10, 
                             np.max(x_axisA) + 1, 
                             np.max([datasetA_stats.max(), datasetB_stats.max()]) + 10)
    
    frame.SetTitle(title)
    frame.GetXaxis().SetTitle("X-Axis Variable")
    frame.GetYaxis().SetTitle("Y-Axis Variable")

    # Draw DatasetA lines and whiskers
    whisker_length = 0.5  # Adjusted for better visual spacing
    datasetA_ref_line = None
    for i, x_value in enumerate(x_axisA):
        min_val, q1_val, median_val, q3_val, max_val = datasetA_stats[i]

        # Draw vertical line for DatasetA
        line = ROOT.TLine(x_value, min_val, x_value, max_val)
        line.SetLineColor(ROOT.kBlue)
        line.SetLineWidth(2)
        line.Draw("SAME")
        
        if i == 0:
            datasetA_ref_line = line

        # Draw horizontal whiskers for min and max
        min_whisker = ROOT.TLine(x_value - whisker_length, min_val, x_value + whisker_length, min_val)
        max_whisker = ROOT.TLine(x_value - whisker_length, max_val, x_value + whisker_length, max_val)
        min_whisker.SetLineColor(ROOT.kBlue)
        max_whisker.SetLineColor(ROOT.kBlue)
        min_whisker.Draw("SAME")
        max_whisker.Draw("SAME")

        # Draw median marker for DatasetA
        marker = ROOT.TMarker(x_value, median_val, 20)
        marker.SetMarkerColor(ROOT.kBlue)
        marker.Draw("SAME")

    # Draw DatasetB lines and whiskers
    datasetB_ref_line = None
    for i, x_value in enumerate(x_axisA):
        min_val, q1_val, median_val, q3_val, max_val = datasetB_stats[i]

        # Draw vertical line for DatasetB
        line = ROOT.TLine(x_value, min_val, x_value, max_val)
        line.SetLineColor(ROOT.kGreen)
        line.SetLineWidth(2)
        line.Draw("SAME")
        
        if i == 0:
            datasetB_ref_line = line

        # Draw horizontal whiskers for min and max
        min_whisker = ROOT.TLine(x_value - whisker_length, min_val, x_value + whisker_length, min_val)
        max_whisker = ROOT.TLine(x_value - whisker_length, max_val, x_value + whisker_length, max_val)
        min_whisker.SetLineColor(ROOT.kGreen)
        max_whisker.SetLineColor(ROOT.kGreen)
        min_whisker.Draw("SAME")
        max_whisker.Draw("SAME")

        # Draw median marker for DatasetB
        marker = ROOT.TMarker(x_value, median_val, 20)
        marker.SetMarkerColor(ROOT.kGreen)
        marker.Draw("SAME")

    # Add legend using the first line for each dataset
    legend = ROOT.TLegend(0.15, 0.65, 0.35, 0.85)
    legend.AddEntry(datasetA_ref_line, "DatasetA", "l")
    legend.AddEntry(datasetB_ref_line, "DatasetB", "l")
    legend.Draw()
    canvas.Modified()
    canvas.Update()

    # Save the canvas as an image file
    canvas.SaveAs(output_file)

    # Clean up
    canvas.Close()
    del canvas

    print(f"Boxplot saved to {output_file}")

# Sample data dictionary for testing purposes
sample_data = {
    'Exp10': {'datasetA': {'min': 1, 'q1': 2, 'median': 3, 'q3': 4, 'max': 5},
              'datasetB': {'min': 2, 'q1': 3, 'median': 4, 'q3': 5, 'max': 6}},
    'Exp20': {'datasetA': {'min': 1.5, 'q1': 2.5, 'median': 3.5, 'q3': 4.5, 'max': 5.5},
              'datasetB': {'min': 2.5, 'q1': 3.5, 'median': 4.5, 'q3': 5.5, 'max': 6.5}},
    'Exp30': {'datasetA': {'min': 2, 'q1': 3, 'median': 4, 'q3': 5, 'max': 6},
              'datasetB': {'min': 3, 'q1': 4, 'median': 5, 'q3': 6, 'max': 7}},
}

# Running the function with sample data
create_stats_plot(sample_data)

ROOT Version: 6.32.00
Platform: linuxx8664gcc
Compiler: GCC


Maybe @couet can help you with this

In princple Modified() and Update() should for the redrawing. On Mac we need
also gSystem->ProcessEvents(); but you are on linux. May be try nevertheless .

Dear @mgamero ,

Thanks for reaching out to the forum! From quickly inspecting your code, I’m suspecting the reason you only see the last iteration of drawing is that the objects you are trying to draw go out of scope and they are lost at each iteration. Try storing your objects such as min_whisker, max_whisker and similar that you use inside the for loop in an outer list, like

myobjects = []
for ...:
    min_whisker = ROOT.TLine(...) 
    min_whisker.Draw()
    myobjects.append(min_whisker)

Let me know if this helps,
Vincenzo