diff --git a/quickstats/_version.py b/quickstats/_version.py
index fe78b13f2df3dba3420174b30ac3945c0b79c497..3ebcf686435c5946013707a98db09434398fb09c 100644
--- a/quickstats/_version.py
+++ b/quickstats/_version.py
@@ -1 +1 @@
-__version__ = "0.7.0.2"
+__version__ = "0.7.0.3"
diff --git a/quickstats/analysis/data_preprocessing.py b/quickstats/analysis/data_preprocessing.py
index 4fa822669a0bf73ad1f2287a9068b6561ae89c2f..a955e37cd27a8c1b579a00562aa3abdf842cdbcb 100644
--- a/quickstats/analysis/data_preprocessing.py
+++ b/quickstats/analysis/data_preprocessing.py
@@ -16,9 +16,9 @@ def fix_negative_weights(df, mode:Union[int, NegativeWeightMode, str]=0,
         return None
     mask = df[weight_col] < 0
     if mode == NegativeWeightMode.SETZERO:
-        df[weight_col][mask] = 0
+        df.loc[mask, weight_col] = 0
     elif mode == NegativeWeightMode.SETABS:
-        df[weight_col][mask] = abs(df[weight_col][mask])
+        df.loc[mask, weight_col] = abs(df[weight_col][mask])
         
 def shuffle_arrays(*arrays, random_state:Optional[int]=None):
     if random_state < 0:
diff --git a/quickstats/analysis/ntuple_process_tool.py b/quickstats/analysis/ntuple_process_tool.py
index 1b4082d58e5f79d8b728ccf8f9f92e483dd3d080..0ad49e243b90264a9c6b7e9d977378d6f57cfa24 100644
--- a/quickstats/analysis/ntuple_process_tool.py
+++ b/quickstats/analysis/ntuple_process_tool.py
@@ -92,6 +92,7 @@ class NTupleProcessTool(ConfigurableObject):
             self.process_flags = []
         
         self.cutflow_report = None
+        self.process_metadata = {}
         
     def load_sample_config(self, config_source:Union[Dict, str]):
         if isinstance(config_source, str):
@@ -396,7 +397,16 @@ class NTupleProcessTool(ConfigurableObject):
                 self.processor.global_variables['outdir'] = outdir
                 self.prerun_process(sample_config)
                 self.processor.run(sample_paths)
+                self.set_process_metadata(sample, sample_type,
+                                          self.processor.result_metadata.copy())
                 self.processor.clear_global_variables()
+
+    def set_process_metadata(self, sample:str,
+                             sample_type:str,
+                             metadata:Dict):
+        if sample not in self.process_metadata:
+            self.process_metadata[sample] = {}
+        self.process_metadata[sample][sample_type] = metadata
                 
     def merge_outputs(self, source_path_func:Callable,
                       target_path_func:Callable,
@@ -645,4 +655,27 @@ class NTupleProcessTool(ConfigurableObject):
 
             fig.suptitle(f"Sample: {sample}", fontsize=20, y=0.98)
             
-            plt.show()
\ No newline at end of file
+            plt.show()
+
+    def copy_remote_samples(self, samples:Optional[Union[List[str], str]]=None,
+                            sample_types:Optional[Union[List[str], str]]=None,
+                            cache:bool=True,
+                            cachedir:str='/tmp',
+                            parallel:bool=False):
+        if isinstance(samples, str):
+            samples = [samples]
+        if isinstance(sample_types, str):
+            sample_types = [sample_types]
+        paths = self.get_selected_paths(syst_themes=['Nominal'],
+                                        samples=samples,
+                                        sample_types=sample_types)
+        if not paths:
+            self.stdout.warning('No inputs matching the given conditions. Skipped.')
+        paths = paths['Nominal']
+        filenames = []
+        for sample in paths:
+            for sample_type in paths[sample]:
+                filenames.extend(paths[sample][sample_type])
+        from quickstats.interface.root import TFile
+        TFile.copy_remote_files(filenames, cache=cache,
+                                cachedir=cachedir, parallel=parallel)
\ No newline at end of file
diff --git a/quickstats/components/__init__.py b/quickstats/components/__init__.py
index f2b2513a95ab93512a3a45f1208dd7e5339b6c09..ba5e81eb630cc84dd15910989facfe533870f97b 100644
--- a/quickstats/components/__init__.py
+++ b/quickstats/components/__init__.py
@@ -1,3 +1,6 @@
+import quickstats
+quickstats.core.methods._require_module("ROOT", quickstats.core.methods.is_root_installed)
+
 from .basics import *
 from .root_object import ROOTObject
 from .discrete_nuisance import DiscreteNuisance
diff --git a/quickstats/components/likelihood.py b/quickstats/components/likelihood.py
index a09b60de16e2bd22a1d5fe4e049f571d16847a08..6a8f6d8dfa4dbc91d1140ea287842e0a3498e362 100644
--- a/quickstats/components/likelihood.py
+++ b/quickstats/components/likelihood.py
@@ -50,24 +50,19 @@ class Likelihood(AnalysisObject):
         poi_val = cond_fit_result['mu']
         if (qmu >= 0):
             ndof = len(poi_val)
-            if ndof == 1:
-                # ndof = 1 case
-                poi_name = list(poi_val)[0]
-                x0 = poi_val[poi_name]
-                if x0 == 0:
-                    sign = np.sign(uncond_fit_result['muhat'][poi_name])
-                    significance = sign * math.sqrt(qmu)
+            poi_name = list(poi_val)[0]
+            mu = poi_val[poi_name]
+            mu_hat = uncond_fit_result['muhat'][poi_name]
+            # one-sided p-value
+            if mu == 0:
+                if (ndof == 1) and (mu_hat < 0):
+                    pvalue = ROOT.Math.normal_cdf_c(-math.sqrt(qmu))
                 else:
-                    significance = math.sqrt(qmu)
-                pvalue = 1 - ROOT.Math.normal_cdf(significance, 1, x0)
+                    pvalue = ROOT.Math.chisquared_cdf_c(qmu, ndof) / 2
+            # two-sided p-value
             else:
-                x0 = list(set(poi_val.values()))
-                if len(x0) > 1:
-                    pvalue = None
-                    significance = None
-                else:
-                    pvalue = ROOT.Math.chisquared_cdf_c(qmu, ndof, x0[0])
-                    significance = ROOT.RooStats.PValueToSignificance(pvalue)
+                pvalue = ROOT.Math.chisquared_cdf_c(qmu, ndof)
+            significance = ROOT.RooStats.PValueToSignificance(pvalue)
             combined_fit_result['significance'] = significance
             combined_fit_result['pvalue'] = pvalue
         else:
diff --git a/quickstats/components/processors/actions/formatter.py b/quickstats/components/processors/actions/formatter.py
index eed7656ced878c4e6b1cfda4b1ff1cc9cfad055b..156dc579692eb09d251b3555447938d8d28e35c0 100644
--- a/quickstats/components/processors/actions/formatter.py
+++ b/quickstats/components/processors/actions/formatter.py
@@ -1,6 +1,6 @@
 import re
 
-from quickstats.utils.string_utils import split_str
+from quickstats.utils.string_utils import split_str, str_to_bool
 
 ListRegex = re.compile(r"\[([^\[\]]+)\]")
 
@@ -8,4 +8,7 @@ def ListFormatter(text:str):
     match = ListRegex.match(text)
     if not match:
         return [text]
-    return split_str(match.group(1), sep=',', strip=True, remove_empty=True)
\ No newline at end of file
+    return split_str(match.group(1), sep=',', strip=True, remove_empty=True)
+
+def BoolFormatter(text:str):
+    return str_to_bool(text)
\ No newline at end of file
diff --git a/quickstats/components/processors/actions/rooproc_alias.py b/quickstats/components/processors/actions/rooproc_alias.py
index 38dc36230f99f6fbb7955059f4b2e4ab66778b31..bb6027e6ec8da189f02258266fd6f1670b940277 100644
--- a/quickstats/components/processors/actions/rooproc_alias.py
+++ b/quickstats/components/processors/actions/rooproc_alias.py
@@ -1,11 +1,11 @@
-from typing import Optional
+from typing import Optional, Dict
 import re
 
-from .rooproc_rdf_action import RooProcRDFAction
+from .rooproc_hybrid_action import RooProcHybridAction
 from .auxiliary import register_action
 
 @register_action
-class RooProcAlias(RooProcRDFAction):
+class RooProcAlias(RooProcHybridAction):
     
     NAME = "ALIAS"
     
@@ -22,10 +22,20 @@ class RooProcAlias(RooProcRDFAction):
             raise RuntimeError(f"invalid expression {main_text}")
         alias = result.group(1)
         column_name = result.group(2)
-        return cls(alias=alias, column_name=column_name)        
-        
-    def _execute(self, rdf:"ROOT.RDataFrame", **params):
+        return cls(alias=alias, column_name=column_name)
+
+    def _execute(self, rdf:"ROOT.RDataFrame", processor:"quickstats.RooProcessor", **params):
         alias = params['alias']
         column_name = params['column_name']
         rdf_next = rdf.Alias(alias, column_name)
-        return rdf_next
\ No newline at end of file
+        return rdf_next, processor
+
+    def get_referenced_columns(self, global_vars:Optional[Dict]=None):
+        params = self.get_formatted_parameters(global_vars, strict=False)
+        column = params['column_name']
+        return [column]
+
+    def get_defined_columns(self, global_vars:Optional[Dict]=None):
+        params = self.get_formatted_parameters(global_vars, strict=False)
+        column = params['alias']
+        return [column]
\ No newline at end of file
diff --git a/quickstats/components/processors/actions/rooproc_as_hdf.py b/quickstats/components/processors/actions/rooproc_as_hdf.py
index 4161af18bcd6d44f77000822617f9093ce39276d..93f5befa4bf43b883dedc6a7c2701406400ba78b 100644
--- a/quickstats/components/processors/actions/rooproc_as_hdf.py
+++ b/quickstats/components/processors/actions/rooproc_as_hdf.py
@@ -7,7 +7,6 @@ from .auxiliary import register_action
 
 from quickstats import module_exist
 from quickstats.utils.common_utils import is_valid_file
-from quickstats.utils.data_conversion import ConversionMode
 from quickstats.interface.root import RDataFrameBackend
 
 @register_action
@@ -16,7 +15,8 @@ class RooProcAsHDF(RooProcOutputAction):
     NAME = "AS_HDF"
     
     def __init__(self, filename:str, key:str,
-                 columns:Optional[List[str]]):
+                 columns:Optional[List[str]],
+                 exclude:Optional[List[str]]=None):
         super().__init__(filename=filename,
                          columns=columns,
                          key=key)
@@ -31,8 +31,10 @@ class RooProcAsHDF(RooProcOutputAction):
         import awkward as ak
         import pandas as pd
         columns = params.get('columns', None)
-        columns = self.get_valid_columns(rdf, processor, columns=columns,
-                                         mode=ConversionMode.REMOVE_NON_STANDARD_TYPE)
+        exclude = params.get('exclude', None)
+        save_columns = self.get_save_columns(rdf, processor, columns=columns,
+                                             exclude=exclude,
+                                             mode="REMOVE_NON_STANDARD_TYPE")
         array = None
         if module_exist('awkward'):
             try:
@@ -40,14 +42,14 @@ class RooProcAsHDF(RooProcOutputAction):
                 # NB: RDF Dask/Spark does not support GetColumnType yet
                 if processor.backend in [RDataFrameBackend.DASK, RDataFrameBackend.SPARK]:
                     rdf.GetColumnType = rdf._headnode._localdf.GetColumnType
-                array = ak.from_rdataframe(rdf, columns=columns)
+                array = ak.from_rdataframe(rdf, columns=save_columns)
                 array = ak.to_numpy(array)
             except:
                 array = None
                 processor.stdout.warning("Failed to convert output to numpy arrays with awkward backend. "
                                          "Falling back to use ROOT instead")
         if array is None:
-            array = rdf.AsNumpy(columns)
+            array = rdf.AsNumpy(save_columns)
         df = pd.DataFrame(array)
         self.makedirs(filename)
         df.to_hdf(filename, key=key)
diff --git a/quickstats/components/processors/actions/rooproc_as_numpy.py b/quickstats/components/processors/actions/rooproc_as_numpy.py
index d65620ba5f7aa493ca621c61583cca92952ae56c..55b292cf7c98ad483c1efc3effe545c16ee237c7 100644
--- a/quickstats/components/processors/actions/rooproc_as_numpy.py
+++ b/quickstats/components/processors/actions/rooproc_as_numpy.py
@@ -22,8 +22,10 @@ class RooProcAsNumpy(RooProcOutputAction):
             return rdf, processor
         processor.stdout.info(f'Writing output to "{filename}".')
         columns = params.get('columns', None)
-        columns = self.get_valid_columns(rdf, processor, columns=columns,
-                                         mode=ConversionMode.REMOVE_NON_STANDARD_TYPE)
+        exclude = params.get('exclude', None)
+        save_columns = self.get_save_columns(rdf, processor, columns=columns,
+                                             exclude=exclude,
+                                             mode="REMOVE_NON_STANDARD_TYPE")
         array = None
         if module_exist('awkward'):
             try:
@@ -31,14 +33,14 @@ class RooProcAsNumpy(RooProcOutputAction):
                 # NB: RDF Dask/Spark does not support GetColumnType yet
                 if processor.backend in [RDataFrameBackend.DASK, RDataFrameBackend.SPARK]:
                     rdf.GetColumnType = rdf._headnode._localdf.GetColumnType
-                array = ak.from_rdataframe(rdf, columns=columns)
+                array = ak.from_rdataframe(rdf, columns=save_columns)
                 array = ak.to_numpy(array)
             except:
                 array = None
                 processor.stdout.warning("Failed to convert output to numpy arrays with awkward backend. "
                                          "Falling back to use ROOT instead")
         if array is None:
-            array = rdf.AsNumpy(columns)
+            array = rdf.AsNumpy(save_columns)
         self.makedirs(filename)
         np.save(filename, array)
         return rdf, processor
\ No newline at end of file
diff --git a/quickstats/components/processors/actions/rooproc_as_parquet.py b/quickstats/components/processors/actions/rooproc_as_parquet.py
index b627370ee3ebbca35229f954e351901f61c8754f..2422d391c9075ca43e3c5f0b573a38351c279d13 100644
--- a/quickstats/components/processors/actions/rooproc_as_parquet.py
+++ b/quickstats/components/processors/actions/rooproc_as_parquet.py
@@ -21,16 +21,18 @@ class RooProcAsParquet(RooProcOutputAction):
             return rdf, processor
         processor.stdout.info(f'Writing output to "{filename}".')
         columns = params.get('columns', None)
-        columns = self.get_valid_columns(rdf, processor, columns=columns,
-                                         mode=ConversionMode.REMOVE_NON_STANDARD_TYPE)
+        exclude = params.get('exclude', None)
+        save_columns = self.get_save_columns(rdf, processor, columns=columns,
+                                             exclude=exclude,
+                                             mode="REMOVE_NON_STANDARD_TYPE")
         import awkward as ak
         try:
             # NB: RDF Dask/Spark does not support GetColumnType yet
             if processor.backend in [RDataFrameBackend.DASK, RDataFrameBackend.SPARK]:
                 rdf.GetColumnType = rdf._headnode._localdf.GetColumnType
-            array = ak.from_rdataframe(rdf, columns=columns)
+            array = ak.from_rdataframe(rdf, columns=save_columns)
         except:
-            array = ak.Array(rdf.AsNumpy(columns))
+            array = ak.Array(rdf.AsNumpy(save_columns))
         self.makedirs(filename)
         ak.to_parquet(array, filename)
         return rdf, processor
\ No newline at end of file
diff --git a/quickstats/components/processors/actions/rooproc_awkward_array.py b/quickstats/components/processors/actions/rooproc_awkward_array.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/quickstats/components/processors/actions/rooproc_base_action.py b/quickstats/components/processors/actions/rooproc_base_action.py
index 8b7aa2063c167d7c0c812e6982d129d9d12fe133..69101f33b99806115413366a8a8c5dca12e4402b 100644
--- a/quickstats/components/processors/actions/rooproc_base_action.py
+++ b/quickstats/components/processors/actions/rooproc_base_action.py
@@ -4,6 +4,8 @@ import re
 
 from quickstats.utils.py_utils import get_required_args
 
+LITERAL_REGEX = re.compile(r"\${(\w+)}")
+
 class RooProcBaseAction(object):
     
     NAME = None
@@ -20,9 +22,15 @@ class RooProcBaseAction(object):
 
     @staticmethod
     def has_global_var(text:str):
-        return re.search(r"\${(\w+)}", text) is not None
+        if not isinstance(text, str):
+            text = str(text)
+        return LITERAL_REGEX.search(text) is not None
+
+    @staticmethod
+    def _get_literals(s:str):
+        return LITERAL_REGEX.findall(s)
     
-    def get_formatted_parameters(self, global_vars:Optional[Dict]=None):
+    def get_formatted_parameters(self, global_vars:Optional[Dict]=None, strict:bool=True):
         if global_vars is None:
             global_vars = {}
         formatted_parameters = {}
@@ -30,26 +38,35 @@ class RooProcBaseAction(object):
             if v is None:
                 formatted_parameters[k] = None
                 continue
-            k_literals = re.findall(r"\${(\w+)}", k)
+            k_literals = self._get_literals(k)
             is_list = False
             if isinstance(v, list):
                 v = '__SEPARATOR__'.join(v)
                 is_list = True
-            v_literals = re.findall(r"\${(\w+)}", v)
+            elif not isinstance(v, str):
+                formatted_parameters[k] = v
+                continue
+            v_literals = self._get_literals(v)
             all_literals = set(k_literals).union(set(v_literals))
             for literal in all_literals:
-                if literal not in global_vars:
+                if strict and (literal not in global_vars):
                     raise RuntimeError(f"the global variable `{literal}` is undefined")
             for literal in k_literals:
+                if literal not in global_vars:
+                    continue
                 substitute = global_vars[literal]
                 k = k.replace("${" + literal + "}", str(substitute))
             for literal in v_literals:
+                if literal not in global_vars:
+                    continue                
                 substitute = global_vars[literal]
                 v = v.replace("${" + literal + "}", str(substitute))
             if is_list:
                 v = v.split("__SEPARATOR__")
             formatted_parameters[k] = v
         for key, value in formatted_parameters.items():
+            if not isinstance(value, str):
+                continue
             if key in self.PARAM_FORMATS:
                 formatter = self.PARAM_FORMATS[key]
                 formatted_parameters[key] = formatter(value)
@@ -100,4 +117,10 @@ class RooProcBaseAction(object):
             argnames = get_required_args(cls)
             missing_argnames = list(set(argnames) - set(kwargs))
             raise ValueError(f'missing keyword argument(s) for the action "{cls.NAME}": '
-                             f'{", ".join(missing_argnames)}')
\ No newline at end of file
+                             f'{", ".join(missing_argnames)}')
+
+    def get_referenced_columns(self, global_vars:Optional[Dict]=None):
+        return []
+
+    def get_defined_columns(self, global_vars:Optional[Dict]=None):
+        return []
\ No newline at end of file
diff --git a/quickstats/components/processors/actions/rooproc_define.py b/quickstats/components/processors/actions/rooproc_define.py
index 15ff70f12a2de3f7816bc92c0c2610231b56ae3b..ff8a3e8013acadf88ec6a890be5ddd18e5426ba7 100644
--- a/quickstats/components/processors/actions/rooproc_define.py
+++ b/quickstats/components/processors/actions/rooproc_define.py
@@ -1,6 +1,7 @@
-from typing import Optional
+from typing import Optional, Dict
 import re
 
+from quickstats.utils.string_utils import extract_variable_names
 from .rooproc_rdf_action import RooProcRDFAction
 from .auxiliary import register_action
 
@@ -26,4 +27,21 @@ class RooProcDefine(RooProcRDFAction):
         name = params['name']
         expression = params['expression']
         rdf_next = rdf.Define(name, expression)
-        return rdf_next
\ No newline at end of file
+        return rdf_next
+
+    def get_referenced_columns(self, global_vars:Optional[Dict]=None):
+        params = self.get_formatted_parameters(global_vars, strict=False)
+        expr = params['expression']
+        # need to remove global variables from the variable search
+        literals = self._get_literals(expr)
+        for literal in literals:
+            expr = expr.replace("${" + literal + "}", "1")
+        literals = ["${" + literal + "}" for literal in literals]
+        referenced_columns = extract_variable_names(expr)
+        referenced_columns.extend(literals)
+        return referenced_columns
+
+    def get_defined_columns(self, global_vars:Optional[Dict]=None):
+        params = self.get_formatted_parameters(global_vars, strict=False)
+        defined_columns = [params['name']]
+        return defined_columns
\ No newline at end of file
diff --git a/quickstats/components/processors/actions/rooproc_filter.py b/quickstats/components/processors/actions/rooproc_filter.py
index 8a0ef4e775452a65d2e8bad93df2b8a9a133df58..6e653a4041e78159a3059d6826a45948ca210ec0 100644
--- a/quickstats/components/processors/actions/rooproc_filter.py
+++ b/quickstats/components/processors/actions/rooproc_filter.py
@@ -17,8 +17,8 @@ class RooProcFilter(RooProcRDFAction):
     def parse(cls, main_text:str, block_text:Optional[str]=None):
         name_literals = re.findall(r"@{([^{}]+)}", main_text)
         if len(name_literals) == 0:
-            name = None
-            expression = main_text.strip()
+            name = main_text.strip()
+            expression = name
         elif len(name_literals) == 1:
             name = name_literals[0]
             expression = main_text.replace("@{" + name + "}", "").strip()
diff --git a/quickstats/components/processors/actions/rooproc_output_action.py b/quickstats/components/processors/actions/rooproc_output_action.py
index 611531125d02357bc3d00c2647ad834bfe76b647..567f0f6d14613326f3d64a87bbe3251907549d58 100644
--- a/quickstats/components/processors/actions/rooproc_output_action.py
+++ b/quickstats/components/processors/actions/rooproc_output_action.py
@@ -1,4 +1,4 @@
-from typing import Optional, List
+from typing import Optional, List, Dict
 import fnmatch
 
 import numpy as np
@@ -7,17 +7,19 @@ from .rooproc_hybrid_action import RooProcHybridAction
 from .formatter import ListFormatter
 from quickstats.interface.root import RDataFrameBackend
 
-from quickstats.utils.common_utils import is_valid_file
+from quickstats.utils.common_utils import is_valid_file, filter_by_wildcards
 from quickstats.utils.data_conversion import root_datatypes, get_rdf_column_type, ConversionMode, reduce_vector_types
 
 class RooProcOutputAction(RooProcHybridAction):
 
     PARAM_FORMATS = {
-        'columns': ListFormatter
+        'columns': ListFormatter,
+        'exclude': ListFormatter
     }
     
     def __init__(self, filename:str, 
-                 columns:Optional[List[str]],
+                 columns:Optional[List[str]]=None,
+                 exclude:Optional[List[str]]=None,
                  **kwargs):
         super().__init__(filename=filename,
                          columns=columns,
@@ -28,38 +30,45 @@ class RooProcOutputAction(RooProcHybridAction):
         kwargs = cls.parse_as_kwargs(main_text)
         return cls._try_create(**kwargs)
     
-    def get_valid_columns(self, rdf, processor, columns:Optional[List[str]]=None,
-                          mode:ConversionMode=ConversionMode.REMOVE_NON_STANDARD_TYPE):
+    def get_save_columns(self, rdf, processor,
+                         columns:Optional[List[str]]=None,
+                         exclude:Optional[List[str]]=None,
+                         mode:ConversionMode=ConversionMode.REMOVE_NON_STANDARD_TYPE):
         all_columns = list([str(col) for col in rdf.GetColumnNames()])
+        
+        save_columns = filter_by_wildcards(all_columns, columns)
+        save_columns = filter_by_wildcards(save_columns, exclude, exclusion=True)
+        save_columns = list(set(save_columns))
+        
         if columns is None:
-            columns = all_columns
-        else:
-            columns_ = []
-            for column in columns:
-                if "*" in column:
-                    matched_columns = fnmatch.filter(all_columns, column)
-                    if not matched_columns:
-                        processor.stdout.warning(f'No columns matching the expression "{column}". '
-                                                 'It will be excluded from the output')
-                    columns_.extend(matched_columns)
-                elif column not in all_columns:
-                    processor.stdout.warning(f'Column "{column}" does not exist. '
-                                             'It will be excluded from the output')
-                else:
-                    columns_.append(column)
-            columns = columns_
+            columns = list(all_columns)
+        if exclude is None:
+            exclude = []
+
+        save_columns = filter_by_wildcards(all_columns, columns)
+        save_columns = filter_by_wildcards(save_columns, exclude, exclusion=True)
+
         mode = ConversionMode.parse(mode)
         if mode in [ConversionMode.REMOVE_NON_STANDARD_TYPE,
                     ConversionMode.REMOVE_NON_ARRAY_TYPE]:
-            column_types = np.array([get_rdf_column_type(rdf, col) for col in columns])
-
+            column_types = np.array([get_rdf_column_type(rdf, col) for col in save_columns])
             if mode == ConversionMode.REMOVE_NON_ARRAY_TYPE:
                 column_types = reduce_vector_types(column_types)
-            new_columns = list(np.array(columns)[np.where(np.isin(column_types, root_datatypes))])
-            removed_columns = np.setdiff1d(columns, new_columns)
+            new_columns = list(np.array(save_columns)[np.where(np.isin(column_types, root_datatypes))])
+            removed_columns = np.setdiff1d(save_columns, new_columns)
             if len(removed_columns) > 0:
                 col_str = ", ".join(removed_columns)
                 processor.stdout.warning("The following column(s) will be excluded from the output as they have "
                                          f"data types incompatible with the output format: {col_str}")
-            columns = new_columns
+            save_columns = new_columns
+        return save_columns
+
+    def get_referenced_columns(self, global_vars:Optional[Dict]=None):
+        params = self.get_formatted_parameters(global_vars, strict=False)
+        columns = params.get("columns", None)
+        if columns is None:
+            columns = ["*"]
+        exclude = params.get("exclude", None)
+        if exclude is not None:
+            self.stdout.warning("Column exclusion will not be applied when inferring referenced columns")
         return columns
\ No newline at end of file
diff --git a/quickstats/components/processors/actions/rooproc_report.py b/quickstats/components/processors/actions/rooproc_report.py
index d7a60d721e15d4e7c39b68bb9a1fcb72ff3b68c8..c74a24c83650df99c89c1532fdf4bea702a8920f 100644
--- a/quickstats/components/processors/actions/rooproc_report.py
+++ b/quickstats/components/processors/actions/rooproc_report.py
@@ -4,6 +4,7 @@ import pandas as pd
 
 from .rooproc_hybrid_action import RooProcHybridAction
 from .auxiliary import register_action
+from .formatter import BoolFormatter
 
 from quickstats.utils.common_utils import is_valid_file
 
@@ -11,6 +12,10 @@ from quickstats.utils.common_utils import is_valid_file
 class RooProcReport(RooProcHybridAction):
     
     NAME = "REPORT"
+
+    PARAM_FORMATS = {
+        'display': BoolFormatter
+    }    
     
     def __init__(self, display:bool=False, filename:Optional[str]=None):
         super().__init__(display=display,
diff --git a/quickstats/components/processors/actions/rooproc_safe_alias.py b/quickstats/components/processors/actions/rooproc_safe_alias.py
index 9c50e043330a4c5629b7f5834a38cf4d6e62031e..cd16b9080a358d9b0c829f25acb8f8df7f4c543f 100644
--- a/quickstats/components/processors/actions/rooproc_safe_alias.py
+++ b/quickstats/components/processors/actions/rooproc_safe_alias.py
@@ -1,29 +1,14 @@
-from typing import Optional
+from typing import Optional, Dict
 import re
 
-from .rooproc_hybrid_action import RooProcHybridAction
+from .rooproc_alias import RooProcAlias
 from .auxiliary import register_action
 
 @register_action
-class RooProcSafeAlias(RooProcHybridAction):
+class RooProcSafeAlias(RooProcAlias):
     
     NAME = "SAFEALIAS"
     
-    def __init__(self, alias:str, column_name:str):
-        super().__init__(alias=alias, column_name=column_name)
-        
-    @classmethod
-    def parse(cls, main_text:str, block_text:Optional[str]=None):
-        result = re.search(r"^\s*(\w+)\s*=\s*([\w\.\${}]+)\s*$", main_text)
-        if not result:
-            if re.search(r"^\s*(\w+)\s*=\s*([\w\.\${}]+)", main_text):
-                raise RuntimeError(f'can not alias an expression ("{main_text}"), '
-                                   'please use DEFINE instead')
-            raise RuntimeError(f"invalid expression {main_text}")
-        alias = result.group(1)
-        column_name = result.group(2)
-        return cls(alias=alias, column_name=column_name)    
-    
     def _execute(self, rdf:"ROOT.RDataFrame", processor:"quickstats.RooProcessor", **params):
         alias = params['alias']
         column_name = params['column_name']
diff --git a/quickstats/components/processors/actions/rooproc_save.py b/quickstats/components/processors/actions/rooproc_save.py
index ffb8f19c8249f6ca827dde6b33a10f88707138eb..629139eb3c42aed9e94933b8c457c079b4389871 100644
--- a/quickstats/components/processors/actions/rooproc_save.py
+++ b/quickstats/components/processors/actions/rooproc_save.py
@@ -1,30 +1,24 @@
 from typing import Optional, List
 import fnmatch
 
-from .rooproc_hybrid_action import RooProcHybridAction
+from .rooproc_output_action import RooProcOutputAction
 from .auxiliary import register_action
+from .formatter import ListFormatter
 
 from quickstats.utils.common_utils import is_valid_file, filter_by_wildcards
 
 @register_action
-class RooProcSave(RooProcHybridAction):
+class RooProcSave(RooProcOutputAction):
     
     NAME = "SAVE"
     
     def __init__(self, treename:str, filename:str, 
                  columns:Optional[List[str]]=None,
-                 exclude:Optional[List[str]]=None,
-                 frame:Optional[str]=None):
+                 exclude:Optional[List[str]]=None):
         super().__init__(treename=treename,
                          filename=filename,
                          columns=columns,
-                         exclude=exclude,
-                         frame=frame)
-        
-    @classmethod
-    def parse(cls, main_text:str, block_text:Optional[str]=None):
-        kwargs = cls.parse_as_kwargs(main_text)
-        return cls(**kwargs)
+                         exclude=exclude)
     
     def _execute(self, rdf:"ROOT.RDataFrame", processor:"quickstats.RooProcessor", **params):
         treename = params['treename']
@@ -32,20 +26,14 @@ class RooProcSave(RooProcHybridAction):
         if processor.cache and is_valid_file(filename):
             processor.stdout.info(f'INFO: Cached output from "{filename}".')
             return rdf, processor
-        all_columns = [str(c) for c in rdf.GetColumnNames()]
         columns = params.get('columns', None)
         exclude = params.get('exclude', None)
-        self.makedirs(filename)
-        if isinstance(columns, str):
-            columns = self.parse_as_list(columns)
-        if columns is None:
-            columns = list(all_columns)
-        if exclude is None:
-            exclude = []
-        save_columns = filter_by_wildcards(all_columns, columns)
-        save_columns = filter_by_wildcards(save_columns, exclude, exclusion=True)
-        save_columns = list(set(save_columns))
+        save_columns = self.get_save_columns(rdf, processor,
+                                             columns=columns,
+                                             exclude=exclude,
+                                             mode="ALL")
         processor.stdout.info(f'Writing output to "{filename}".')
+        self.makedirs(filename)
         if processor.use_template:
             from quickstats.utils.root_utils import templated_rdf_snapshot 
             rdf_next = templated_rdf_snapshot(rdf, save_columns)(treename, filename, save_columns)
diff --git a/quickstats/components/processors/actions/rooproc_stat.py b/quickstats/components/processors/actions/rooproc_stat.py
index e2719f01c595ddc7cd4980e003e57166a66d9739..0d156a2ad743ec96047c025bba641e64fda7b7ef 100644
--- a/quickstats/components/processors/actions/rooproc_stat.py
+++ b/quickstats/components/processors/actions/rooproc_stat.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import Optional, Dict
 import re
 
 from .rooproc_hybrid_action import RooProcHybridAction
@@ -24,4 +24,9 @@ class RooProcStat(RooProcHybridAction):
         ext_var_name = params['ext_var_name']
         column_name = params['column_name']
         processor.external_variables[ext_var_name] = self._get_func(rdf)(column_name)
-        return rdf, processor
\ No newline at end of file
+        return rdf, processor
+
+    def get_referenced_columns(self, global_vars:Optional[Dict]=None):
+        params = self.get_formatted_parameters(global_vars, strict=False)
+        referenced_columns = [params['column_name']]
+        return referenced_columns
\ No newline at end of file
diff --git a/quickstats/components/processors/roo_process_config.py b/quickstats/components/processors/roo_process_config.py
index 51c44c4a4d02e05416b165e62771de600946d8bf..3d625a40ed790ced11e472a39908f8d609ca4cb2 100644
--- a/quickstats/components/processors/roo_process_config.py
+++ b/quickstats/components/processors/roo_process_config.py
@@ -1,12 +1,13 @@
-from typing import List, Optional
+from typing import List, Optional, Dict
 import os
 import re
 
 from quickstats import semistaticmethod, TVirtualNode, TVirtualTree, stdout
 from quickstats.utils.string_utils import split_lines, split_str
+from quickstats.utils.common_utils import combine_dict, remove_duplicates
 from quickstats.interface.root import RDataFrameBackend
 
-from .actions import RooProcBaseAction, RooProcNestedAction, get_action
+from .actions import RooProcBaseAction, RooProcNestedAction, get_action, RooProcGlobalVariables
 
 def _get_action(name:str, rdf_backend:Optional[str]=None):
     if name.lower() == "alias":
@@ -17,6 +18,9 @@ def _get_action(name:str, rdf_backend:Optional[str]=None):
             name = "DEFINE"
     return get_action(name)
 
+def _format_multiline_string(s:str):
+    return (s.split('\n')[0] + '...') if '\n' in s else s
+
 class ActionNode(TVirtualNode):
     
     def __init__(self, name:Optional[str]=None, level:Optional[int]=0,
@@ -25,6 +29,23 @@ class ActionNode(TVirtualNode):
         super().__init__(name=name, level=level,
                          parent=parent, **data)
         self.action = None
+
+    def __repr__(self):
+        class_name = self.__class__.__name__
+        attributes = {}
+        attributes['name'] = self.name
+        for key in ['main_text', 'block_text']:
+            value = self.data.get(key, None)
+            if not value:
+                continue
+            attributes[key] = _format_multiline_string(value)
+        attributes['level'] = self.level
+        attributes['source'] =  self.data.get('source', '')
+        attributes['start_line_number'] =  self.data.get('start_line_number', '')
+        attributes['end_line_number'] =  self.data.get('end_line_number', '')
+        attributes['children'] = '[...]' if self.children else '[]'
+        attribute_str = ", ".join([f"{k}={v}" for k, v in attributes.items()])
+        return (f"{class_name}({attribute_str})")
         
     def get_context(self):
         source = self.try_get_data("source", None)
@@ -45,7 +66,7 @@ class ActionNode(TVirtualNode):
         main_text  = self.get_data("main_text")
         block_text = self.get_data("block_text")
         action = action_cls.parse(main_text=main_text, block_text=block_text)
-        self.action = action        
+        self.action = action
         
 class ActionTree(TVirtualTree):
     
@@ -64,6 +85,53 @@ class ActionTree(TVirtualTree):
             node = self.get_next()
         self.reset()
 
+    def _get_columns(self, col_func, global_vars:Optional[Dict]=None,
+                     exclude_global:bool=True):
+        # make a copy
+        global_vars = combine_dict(global_vars)
+        current_node = self.current_node
+        self.reset()
+        node = self.get_next()
+        columns = set()
+        while node is not None:
+            action = node.action
+            if action is None:
+                raise RuntimeError(f'Action not set for the node: {node}')            
+            if isinstance(action, RooProcGlobalVariables):
+                params = action.get_formatted_parameters(global_vars)
+                global_vars.update(params)
+            columns |= set(col_func(node, global_vars))
+            if 'phi' in columns:
+                from pdb import set_trace
+                set_trace()
+            node = self.get_next()
+        self.current_node = current_node
+        columns = list(columns)
+        if exclude_global:
+            columns = [col for col in columns \
+                       if not RooProcBaseAction.has_global_var(col)]
+        return columns
+        
+    def get_referenced_columns(self, global_vars:Optional[Dict]=None,
+                               exclude_defined:bool=True,
+                               exclude_global:bool=True):
+        col_func = lambda node, glob_vars_: node.action.get_referenced_columns(glob_vars_)
+        referenced_columns = self._get_columns(col_func, global_vars,
+                                               exclude_global=exclude_global)
+        if exclude_defined:
+            defined_columns = self.get_defined_columns(global_vars=global_vars,
+                                                       exclude_global=False)
+            referenced_columns = [col for col in referenced_columns \
+                                  if col not in defined_columns]
+        return referenced_columns
+
+    def get_defined_columns(self, global_vars:Optional[Dict]=None,
+                            exclude_global:bool=True):
+        col_func = lambda node, glob_vars_: node.action.get_defined_columns(glob_vars_)
+        return self._get_columns(col_func, global_vars,
+                                 exclude_global=exclude_global)
+       
+
 class RooConfigLine(object):
     
     def __init__(self, text:str, line_number:int):
diff --git a/quickstats/components/processors/roo_processor.py b/quickstats/components/processors/roo_processor.py
index 5ad1e1191702080a757d23375403f6d1073301f8..3a886079edfadf2ea1f3fe3c14182e1137b4e4bc 100644
--- a/quickstats/components/processors/roo_processor.py
+++ b/quickstats/components/processors/roo_processor.py
@@ -9,13 +9,27 @@ from .builtin_methods import BUILTIN_METHODS
 from .actions import *
 from .roo_process_config import RooProcessConfig
 
-from quickstats import timer, AbstractObject, PathManager
+from quickstats import timer, AbstractObject, PathManager, GeneralEnum
 from quickstats.interface.root import TFile, RDataFrame, RDataFrameBackend
 from quickstats.interface.xrootd import get_cachedir, set_cachedir, switch_cachedir
-from quickstats.utils.root_utils import declare_expression, close_all_root_files
+from quickstats.utils.root_utils import declare_expression, close_all_root_files, set_multithread
 from quickstats.utils.path_utils import is_remote_path
 from quickstats.utils.common_utils import get_cpu_count
 
+class RDFVerbosity(GeneralEnum):
+    UNSET   = (0, 'kUnset')
+    FATAL   = (1, 'kFatal')
+    ERROR   = (2, 'kError')
+    WARNING = (3, 'kWarning')
+    INFO    = (4, 'kInfo')
+    DEBUG   = (5, 'kDebug')
+
+    def __new__(cls, value:int, key:str):
+        obj = object.__new__(cls)
+        obj._value_ = value
+        obj.key = key
+        return obj
+
 class RooProcessor(AbstractObject):
 
     @property
@@ -44,7 +58,8 @@ class RooProcessor(AbstractObject):
         self.external_variables = {}
         self.default_treename = None
         self.use_template = use_template
-        self.multithread = multithread
+        self.rdf_verbosity = None
+        self.result_metadata = None
         if backend is None:
             self.backend = RDataFrameBackend.DEFAULT
         else:
@@ -52,22 +67,23 @@ class RooProcessor(AbstractObject):
         self.backend_options = backend_options
         self.set_remote_file_options(localize=False,
                                      cachedir=get_cachedir())
-        
+        self.set_profile_options()
         self.load_buildin_functions()
-        
-        if multithread:            
-            if multithread > 1:
-                ROOT.EnableImplicitMT(multithread)
-                num_thread = multithread
-            else:
-                ROOT.EnableImplicitMT()
-                num_thread = get_cpu_count()
-            self.stdout.info(f'Enabled multithreading with {num_thread} threads.')
-        elif ROOT.IsImplicitMTEnabled():
-            ROOT.DisableImplicitMT()
+
+        self.set_multithread(multithread)
         
         if config_source is not None:
             self.load_config(config_source)
+
+    def set_multithread(self, num_threads:Optional[int]=None):
+        if num_threads is None:
+            num_threads = self.multithread
+        num_threads = set_multithread(num_threads)
+        if num_threads is None:
+            self.stdout.info("Disabled multithreading.")
+        else:
+            self.stdout.info(f"Enabled multithreading with {num_threads} threads.")
+        self.multithread = num_threads
             
     def set_cache(self, cache:bool=True):
         self.cache = cache
@@ -82,6 +98,12 @@ class RooProcessor(AbstractObject):
             'copy_options': copy_options
         }
         self.remote_file_options = remote_file_options
+
+    def set_profile_options(self, throughput:bool=False):
+        profile_options = {
+            "throughput": throughput
+        }
+        self.profile_options = profile_options
             
     def load_buildin_functions(self):
         # bug of redefining module from ROOT
@@ -156,7 +178,9 @@ class RooProcessor(AbstractObject):
             raise RuntimeError("action tree not initialized")
         node = self.action_tree.get_next(consider_child=consider_child)
         if node is not None:
-            self.stdout.debug(f'Executing node "{node.name}" defined at line {node.data["start_line_number"]}')
+            source = node.try_get_data("source", None)
+            self.stdout.debug(f'Executing node "{node.name}" defined at line {node.data["start_line_number"]}'
+                             f' (source {source})')
             action = node.action
             return_code = self.run_action(action)
             if return_code == RooProcReturnCode.NORMAL:
@@ -186,37 +210,42 @@ class RooProcessor(AbstractObject):
                                      raise_on_error=False)
         return files
 
-    def _fetch_remote_files(self, filenames:List[str]):
+    def resolve_filenames(self, filenames:Union[List[str], str]):
+        filenames = self.list_files(filenames, resolve_cache=True)
+        if not filenames:
+            return []
+        has_remote_file = self._has_remote_files(filenames)
+        # copy remote files to local storage
+        if has_remote_file and self.remote_file_options['localize']:
+            remote_files = [filename for filename in filenames if is_remote_path(filename)]
+            self._copy_remote_files(remote_files)
+            filenames = self.list_files(filenames, resolve_cache=True)
+        return filenames
+
+    def _copy_remote_files(self, filenames:List[str]):
         opts = self.remote_file_options
         copy_options = opts.get('copy_options', None)
         if copy_options is None:
             copy_options = {}
-        TFile.fetch_remote_files(filenames, cache=opts['cache'],
+        TFile.copy_remote_files(filenames, cache=opts['cache'],
                                  cachedir=opts['cachedir'],
                                  **copy_options)
         
-    def load_rdataframe(self,
-                        filenames:Union[List[str], str],
-                        treename:Optional[str]=None):
-        
-        if treename is None:
-            treename = self.default_treename
+    def load_rdf(self,
+                 filenames:Union[List[str], str],
+                 treename:Optional[str]=None):
             
-        if treename is None:
-            raise RuntimeError("treename is undefined")
-            
-        filenames = self.list_files(filenames, resolve_cache=True)
-        
+        filenames = self.resolve_filenames(filenames)
         if not filenames:
-            self.stdout.info('No files to be processed. Skipped.')
+            self.stdout.info('No files to be processed. Skipping.')
             return None
-            
-        has_remote_file = self._has_remote_files(filenames)
-        # copy remote files to local storage
-        if has_remote_file and self.remote_file_options['localize']:
-            remote_files = [filename for filename in filenames if is_remote_path(filename)]
-            self._fetch_remote_files(remote_files)
-            filenames = self.list_files(filenames, resolve_cache=True)
+        self._filenames = filenames
+
+        if treename is None:
+            treename = self.default_treename
+        if treename is None:
+            treename = TFile._get_main_treename(filenames[0])
+            self.stdout.info(f"Using deduced treename: {treename}")
 
         if len(filenames) == 1:
             self.stdout.info(f'Processing file "{filenames[0]}".')
@@ -236,19 +265,58 @@ class RooProcessor(AbstractObject):
         self.sanity_check()
         with timer() as t:
             if filenames is not None:
-                self.load_rdataframe(filenames)
+                self.load_rdf(filenames)
             self.action_tree.reset()
             self.run_all_actions()
             self.shallow_cleanup()
         self.stdout.info(f"Task finished. Total time taken: {t.interval:.3f} s.")
+        result_metadata = {
+            "files": list(self._filenames),
+            "real_time": t.real_time_elapsed,
+            "cpu_time": t.cpu_time_elapsed
+        }
+        self.result_metadata = result_metadata
         return self
+
+    def get_rdf(self, frame:Optional[str]=None):
+        rdf = self.rdf if frame is None else self.rdf_frames.get(frame, None)
+        if rdf is None:
+            raise RuntimeError('RDataFrame instance not initialized')
+        return rdf
+
+    def get_referenced_columns(self):
+        action_tree = self.action_tree
+        return action_tree.get_referenced_columns(self.global_variables)
         
     def awkward_array(self, frame:Optional[str]=None,
-                       columns:Optional[List[str]]=None):
-        if frame is None:
-            rdf = self.rdf
+                      columns:Optional[List[str]]=None):
+        rdf = self.get_rdf(frame)
+        return RDataFrame._awkward_array(rdf, columns=columns)
+
+    def display(self, frame:Optional[str]=None,
+                columns:Union[str, List[str]]="",
+                n_rows:int=5, n_max_collection_elements:int=10,
+                lazy:bool=False):
+        rdf = self.get_rdf(frame)
+        result = self.rdf.Display(columns, n_rows, n_max_collection_elements)
+        if not lazy:
+            result.Print()
+            return None
+        return result
+
+    def save_graph(self, frame:Optional[str]=None,
+                   filename:Optional[str]=None):
+        rdf = self.get_rdf(frame)
+        if filename:
+            ROOT.RDF.SaveGraph(rdf, filename)
         else:
-            rdf = self.rdf_frames.get(frame, None)
-        if rdf is None:
-            raise RuntimeError('RDataFrame instance not initialized')
-        return RDataFrame._awkward_array(rdf, columns=columns)
\ No newline at end of file
+            ROOT.RDF.SaveGraph(rdf)
+
+    def set_rdf_verbosity(self, verbosity:str='INFO'):
+        if isinstance(verbosity, str):
+            verbosity = RDFVerbosity.parse(verbosity)
+            loglevel = getattr(ROOT.Experimental.ELogLevel, verbosity.key)
+        else:
+            loglevel = verbosity
+        verb = ROOT.Experimental.RLogScopedVerbosity(ROOT.Detail.RDF.RDFLogChannel(), loglevel)
+        self.rdf_verbosity = verb
\ No newline at end of file
diff --git a/quickstats/concurrent/parameterised_asymptotic_cls.py b/quickstats/concurrent/parameterised_asymptotic_cls.py
index 306fe774171f28d7906572b42ee4c579b2768286..2e579b88a99df3953a4a53b3e7be5791f9bd7550 100644
--- a/quickstats/concurrent/parameterised_asymptotic_cls.py
+++ b/quickstats/concurrent/parameterised_asymptotic_cls.py
@@ -7,7 +7,7 @@ from itertools import repeat
 from quickstats import semistaticmethod
 from quickstats.parsers import ParamParser
 from quickstats.concurrent import ParameterisedRunner
-from quickstats.utils.common_utils import batch_makedirs, json_load, combine_dict, save_as_json
+from quickstats.utils.common_utils import batch_makedirs, json_load, combine_dict, save_json
 from quickstats.components import AsymptoticCLs
 
 class ParameterisedAsymptoticCLs(ParameterisedRunner):
@@ -117,4 +117,4 @@ class ParameterisedAsymptoticCLs(ParameterisedRunner):
         
         if outname is not None:
             outpath = os.path.join(outdir, outname)
-            save_as_json(final_result, outpath)
\ No newline at end of file
+            save_json(final_result, outpath)
\ No newline at end of file
diff --git a/quickstats/concurrent/parameterised_likelihood.py b/quickstats/concurrent/parameterised_likelihood.py
index 6f7439ac399ee6a14fee0d7cfcca2eda01ff1b90..31ef480816a1e1828a9ff7773291a06a851c3ac7 100644
--- a/quickstats/concurrent/parameterised_likelihood.py
+++ b/quickstats/concurrent/parameterised_likelihood.py
@@ -10,7 +10,7 @@ import ROOT
 from quickstats import semistaticmethod
 from quickstats.parsers import ParamParser
 from quickstats.concurrent import ParameterisedRunner
-from quickstats.utils.common_utils import batch_makedirs, save_as_json
+from quickstats.utils.common_utils import batch_makedirs, save_json
 from quickstats.components import Likelihood
 
 class ParameterisedLikelihood(ParameterisedRunner):
@@ -235,4 +235,4 @@ class ParameterisedLikelihood(ParameterisedRunner):
         outdir  = self.attributes['outdir']
         outname = self.attributes['outname'].format(poi_names="_".join(poi_names))
         outpath = os.path.join(outdir, outname.format(poi_name=poi_name))
-        save_as_json(data, outpath)
\ No newline at end of file
+        save_json(data, outpath)
\ No newline at end of file
diff --git a/quickstats/concurrent/parameterised_significance.py b/quickstats/concurrent/parameterised_significance.py
index 646573dd4a68cbfdd864c670ebc2756d920f6e58..38e61ee9df987bc27750914e233cefe975dbf2a7 100644
--- a/quickstats/concurrent/parameterised_significance.py
+++ b/quickstats/concurrent/parameterised_significance.py
@@ -6,7 +6,7 @@ from typing import Optional, Union, Dict, List, Any
 from quickstats import semistaticmethod
 from quickstats.parsers import ParamParser
 from quickstats.concurrent import ParameterisedRunner
-from quickstats.utils.common_utils import batch_makedirs, list_of_dict_to_dict_of_list, save_as_json, combine_dict
+from quickstats.utils.common_utils import batch_makedirs, list_of_dict_to_dict_of_list, save_json, combine_dict
 from quickstats.maths.numerics import pretty_value
 from quickstats.components import AnalysisBase, AsimovType, AsimovGenerator
 
@@ -84,9 +84,11 @@ class ParameterisedSignificance(ParameterisedRunner):
                 asimov_snapshot = AsimovGenerator.ASIMOV_SETTINGS[asimov_type]['asimov_snapshot']
                 analysis.set_data(asimov_data)
                 result = analysis.nll_fit(poi_val=mu_exp, mode='hybrid',
-                                          snapshot_name=asimov_snapshot, do_minos=config['minos'] if "minos" in config else False)
+                                          snapshot_name=asimov_snapshot,
+                                          do_minos=config.get('minos', None))
             else:
-                result = analysis.nll_fit(poi_val=mu_exp, mode='hybrid', do_minos=config['minos'] if "minos" in config else False)
+                result = analysis.nll_fit(poi_val=mu_exp, mode='hybrid',
+                                          do_minos=config.get('minos', None))
             if outname:
                 with open(outname, 'w') as outfile:
                     json.dump(result, outfile, indent=2)
@@ -164,4 +166,4 @@ class ParameterisedSignificance(ParameterisedRunner):
         outdir  = self.attributes['outdir']
         outname = self.attributes['outname'].format(param_names="_".join(param_names))
         outpath = os.path.join(outdir, outname)
-        save_as_json(data, outpath)
\ No newline at end of file
+        save_json(data, outpath)
\ No newline at end of file
diff --git a/quickstats/core/__init__.py b/quickstats/core/__init__.py
index c01ae6427e44b7081c13263c296a5927bfcdefd3..9d9d1b60059b2b836e852299545aa03f128c829c 100644
--- a/quickstats/core/__init__.py
+++ b/quickstats/core/__init__.py
@@ -4,7 +4,7 @@ from .abstract_object import AbstractObject
 from .enums import GeneralEnum, DescriptiveEnum
 from .virtual_trees import TVirtualNode, TVirtualTree
 from .path_manager import DynamicFilePath, PathManager
-from .configurations import *
+from .configuration import *
 #from .configs import ConfigComponent, ConfigParser, ConfigurableObject
 from .methods import *
 from .setup import *
\ No newline at end of file
diff --git a/quickstats/core/configurations.py b/quickstats/core/configuration.py
similarity index 99%
rename from quickstats/core/configurations.py
rename to quickstats/core/configuration.py
index 0e2a4bd9fafae4b70657dee3c901e4ff8a6a86a3..4ef0c91fc42936021f6452c39b817efd4211b6e2 100644
--- a/quickstats/core/configurations.py
+++ b/quickstats/core/configuration.py
@@ -18,7 +18,7 @@ from .type_validation import get_type_validator, get_type_hint_str
 from quickstats.utils.string_utils import format_dict_to_string
 
 
-__all__ = ['as_dict', 'ConfigComponent', 'ConfigScheme', 'ConfigFile', 'ConfigurableObject', 'ConfigUnit']
+__all__ = ['ConfigComponent', 'ConfigScheme', 'ConfigFile', 'ConfigurableObject', 'ConfigUnit']
 
 
 class MISSING_TYPE:
diff --git a/quickstats/core/decorators.py b/quickstats/core/decorators.py
index 52f4fe117366c05d04649cf2826ed05c0f2e4925..3403bc7f6646982a590ea96ba9d9cbf8c2281581 100644
--- a/quickstats/core/decorators.py
+++ b/quickstats/core/decorators.py
@@ -3,6 +3,8 @@ from functools import partial
 import time
 import importlib
 
+__all__ = ["semistaticmethod", "cls_method_timer", "timer"]
+
 class semistaticmethod(object):
     """
     Descriptor to allow a staticmethod inside a class to use 'self' when called from an instance.
@@ -110,7 +112,8 @@ class timer:
         Returns:
             timer: The timer instance itself.
         """
-        self.start = time.time()
+        self.start_real = time.time()
+        self.start_cpu = time.process_time()
         return self
 
     def __exit__(self, *args):
@@ -123,5 +126,8 @@ class timer:
         Returns:
             None
         """
-        self.end = time.time()
-        self.interval = self.end - self.start
\ No newline at end of file
+        self.end_cpu = time.process_time()
+        self.end_real = time.time()
+        self.interval = self.end_real - self.start_real
+        self.real_time_elapsed = self.interval
+        self.cpu_time_elapsed = self.end_cpu - self.start_cpu
\ No newline at end of file
diff --git a/quickstats/core/enums.py b/quickstats/core/enums.py
index 1163ccb93a5d5667ae6ad30387da6c65ff5af3f9..abf6af92495aa05d7b6eaff826702bd50d3c0997 100644
--- a/quickstats/core/enums.py
+++ b/quickstats/core/enums.py
@@ -1,6 +1,8 @@
 from typing import Any, Optional, Union, List, Dict
 from enum import Enum
 
+__all__ = ["GeneralEnum", "DescriptiveEnum"]
+
 class GeneralEnum(Enum):
     """
     Extended Enum class with additional parsing and lookup functionalities.
diff --git a/quickstats/core/methods.py b/quickstats/core/methods.py
index e2eb9232c245dadfd806aa50d406c1308269ed40..dbe1497117f10cdbecca9d6b7dc33e5409be0340 100644
--- a/quickstats/core/methods.py
+++ b/quickstats/core/methods.py
@@ -1,4 +1,4 @@
-from typing import List, Union, Optional, Dict
+from typing import List, Union, Optional, Dict, Callable
 import os
 import glob
 import json
@@ -73,6 +73,9 @@ def get_root_version():
         root_version = ROOTVersion((0, 0, 0))
     return root_version
 
+def is_root_installed(name:str=None):
+    return get_root_version() > (0, 0, 0)
+
 def get_workspace_extensions():
     extension_config = get_workspace_extension_config()
     extensions = extension_config['required']
@@ -162,5 +165,11 @@ def load_processor_methods():
     for name, definition in BUILTIN_METHODS.items():
         declare_expression(definition, name)
         
-def module_exist(name:str):
-    return importlib.util.find_spec(name) is not None
\ No newline at end of file
+def module_exist(name: str) -> bool:
+    return importlib.util.find_spec(name) is not None
+
+def _require_module(name: str, fn:Optional[Callable]=None):
+    if fn is None:
+        fn = module_exist
+    if not fn(name):
+        raise ImportError(f"The module '{name}' is required but not found. Please install it to proceed.")
\ No newline at end of file
diff --git a/quickstats/interface/cppyy/__init__.py b/quickstats/interface/cppyy/__init__.py
index 073e2c8dfb58afd0ecedc199da0d3532fcf0bb93..6792eac1ddba2c6c2af3136c71013a00263056c6 100644
--- a/quickstats/interface/cppyy/__init__.py
+++ b/quickstats/interface/cppyy/__init__.py
@@ -1,3 +1,6 @@
+import quickstats
+quickstats.core.methods._require_module("cppyy")
+
 from quickstats.interface.cppyy.core import *
 from quickstats.interface.cppyy.macros import load_macros, load_macro
 
diff --git a/quickstats/interface/kerberos/__init__.py b/quickstats/interface/kerberos/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ce5123d3912d081a80ed084bce050f69e807b91
--- /dev/null
+++ b/quickstats/interface/kerberos/__init__.py
@@ -0,0 +1,5 @@
+import quickstats
+
+from .core import *
+
+quickstats.methods._require_module("kerberos", is_kerberos_installed)
\ No newline at end of file
diff --git a/quickstats/interface/kerberos/core.py b/quickstats/interface/kerberos/core.py
new file mode 100644
index 0000000000000000000000000000000000000000..a95fa9935d2aed5fdaee45793efef2b66a501ae3
--- /dev/null
+++ b/quickstats/interface/kerberos/core.py
@@ -0,0 +1,49 @@
+import os
+import subprocess
+
+import quickstats
+
+__all__ = ["is_kerberos_installed", "get_kerberos_ticket_cache",
+           "kerberos_ticket_exists", "list_service_principals"]
+
+def is_kerberos_installed(name:str=None):
+    try:
+        subprocess.run(['klist', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
+        return True
+    except FileNotFoundError:
+        return False
+    except subprocess.CalledProcessError:
+        return True
+    except Exception as e:
+        return False
+        
+def get_kerberos_ticket_cache():
+    krb_cache = os.getenv('KRB5CCNAME')
+    if krb_cache:
+        return krb_cache
+    # Fallback to default path if KRB5CCNAME is not set
+    uid = os.getuid()
+    return f"/tmp/krb5cc_{uid}"
+
+def kerberos_ticket_exists():
+    ticket_cache = get_kerberos_ticket_cache()
+    return os.path.exists(ticket_cache)
+
+def list_service_principals():
+    list_service_principals = []
+    try:
+        result = subprocess.run(['klist'], capture_output=True, text=True, check=True)
+        lines = result.stdout.split('\n')
+        for line in lines:
+            if '@' not in line:
+                continue
+            tokens = line.split()
+            if len(tokens) > 3 and '@' in tokens[-1]:
+                list_service_principals.append(tokens[-1]) 
+    except subprocess.CalledProcessError:
+        quickstats.stdout.error("Failed to list tickets - are you sure Kerberos is configured correctly?")
+    except FileNotFoundError:
+        quickstats.stdout.error("Kerberos 'klist' command not found. Is Kerberos installed?")
+    except Exception as e:
+        quickstats.stdout.error(f"An unexpected error occurred: {e}")
+    return list_service_principals
\ No newline at end of file
diff --git a/quickstats/interface/root/TFile.py b/quickstats/interface/root/TFile.py
index cd78b55e5d2367aac482b0c64b60c09ca77f1e0d..402016bec743ae8fcf46c6e433fe28651cafbdd9 100644
--- a/quickstats/interface/root/TFile.py
+++ b/quickstats/interface/root/TFile.py
@@ -7,11 +7,12 @@ import numpy as np
 
 from quickstats import semistaticmethod
 from quickstats.utils.path_utils import (resolve_paths, is_remote_path, remote_glob,
-                                         remote_isdir, remote_dirlist, dirlist,
-                                         local_file_exist, split_url)
+                                         remote_isdir, remote_listdir, listdir,
+                                         local_file_exist, split_url,
+                                         remote_file_exist)
 from quickstats.utils.root_utils import is_corrupt
 from quickstats.utils.common_utils import in_notebook
-from quickstats.interface.xrootd import get_cachedir
+from quickstats.utils.sys_utils import bytes_to_readable
 from .TObject import TObject
 
 class TFile(TObject):
@@ -29,6 +30,39 @@ class TFile(TObject):
     def is_corrupt(f:Union["ROOT.TFile", str]):
         return is_corrupt(f)
 
+    @semistaticmethod
+    def _get_all_treenames(self, source:Union["ROOT.TFile", str]):
+        import ROOT
+        if isinstance(source, str):
+            source = ROOT.TFile.Open(source)
+        keys = [key.GetName() for key in source.GetListOfKeys()]
+        objs = [source.Get(key) for key in keys]
+        trees = [obj for obj in objs if isinstance(obj, ROOT.TTree)]
+        treenames = [tree.GetName() for tree in trees]
+        return treenames
+
+    @semistaticmethod
+    def _get_main_treename(self, source:Union["ROOT.TFile", str]):
+        import ROOT
+        if isinstance(source, str):
+            source = ROOT.TFile.Open(source)
+        keys = [key.GetName() for key in source.GetListOfKeys()]
+        objs = [source.Get(key) for key in keys]
+        trees = [obj for obj in objs if isinstance(obj, ROOT.TTree)]
+        if not trees:
+            raise RuntimeError('no tree found in the root file')
+        elif len(trees) == 1:
+            return trees[0].GetName()
+        main_trees = [tree for tree in trees if tree.GetEntriesFast() > 1]
+        main_treenames = [tree.GetName() for tree in main_trees]
+        if not main_trees:
+            raise RuntimeError('no tree found with entries > 1')
+        elif len(main_trees) > 1:
+            raise RuntimeError('found multiple trees with entries > 1 : {names}'.format(
+                names=", ".join(main_treenames)))
+        return main_treenames[0]
+                               
+
     @semistaticmethod
     def _is_valid_filename(self, filename:str):
         return self.FILE_PATTERN.match(filename) is not None
@@ -54,6 +88,7 @@ class TFile(TObject):
                                      strict_format:Optional[bool]=True,
                                      cached_only:bool=False):
         import ROOT
+        from quickstats.interface.xrootd import get_cachedir
         cachedir = get_cachedir()
         if cachedir is None:
             return list(paths)
@@ -68,7 +103,7 @@ class TFile(TObject):
             cache_path = os.path.join(cachedir, filename)
             if os.path.exists(cache_path):
                 if os.path.isdir(cache_path):
-                    cache_paths = dirlist(cache_path)
+                    cache_paths = listdir(cache_path)
                     if strict_format:
                         cache_paths = self._filter_valid_filenames(cache_paths)
                     if not cache_paths:
@@ -88,28 +123,30 @@ class TFile(TObject):
                    resolve_cache:bool=False,
                    expand_remote_files:bool=True,
                    raise_on_error:bool=True):
-        remote_flag = True
         paths = resolve_paths(paths)
         filenames = []
         
+        if resolve_cache:
+            paths = self._resolve_cached_remote_paths(paths)
+            
         # expand directories if necessary
         for path in paths:
             if is_remote_path(path):
                 if local_file_exist(path):
                     host, path = split_url(path)
-                else:
-                    if remote_flag:
-                        self.stdout.info("Resolving remote files. Network traffic overhead might be expected.")
-                        remote_flag = False
+                elif remote_file_exist(path):
                     if expand_remote_files and remote_isdir(path):
-                        filenames.extend(remote_dirlist(path))
+                        filenames.extend(remote_listdir(path))
                     else:
                         filenames.append(path)
-                    continue
-            if os.path.isdir(path):
-                filenames.extend(dirlist(path))
-            else:
+                else:
+                    self.stdout.warning(f'Remote file "{path}" does not exist')
+            elif os.path.isdir(path):
+                filenames.extend(listdir(path))
+            elif os.path.exists(path):
                 filenames.append(path)
+            else:
+                self.stdout.warning(f'Local file "{path}" does not exist')
         if strict_format:
             filenames = self._filter_valid_filenames(filenames)
         if not filenames:
@@ -117,8 +154,7 @@ class TFile(TObject):
         if resolve_cache:
             filenames = self._resolve_cached_remote_paths(filenames)
         import ROOT
-        invalid_filenames = []
-        valid_filenames = []
+        invalid_filenames, valid_filenames = [], []
         for filename in filenames:
             if is_remote_path(filename):
                 # delay the check of remote root file to when they are open
@@ -138,8 +174,6 @@ class TFile(TObject):
                 raise RuntimeError(f'Found empty/currupted file(s):\n{fmt_str}')
             else:
                 self.stdout.warning(f'Found empty/currupted file(s):\n{fmt_str}')
-        if not remote_flag:
-            self.stdout.info("Finished resolving remote files.")
         return valid_filenames
     
     @staticmethod
@@ -183,10 +217,39 @@ class TFile(TObject):
             return None
         return tree
 
+    def get_tree_compression_summary(self, treename:Optional[str]=None):
+        file = self.obj
+        if treename is None:
+            treename = self._deduce_treename(file)
+        tree = file.Get(treename)
+        total_bytes = tree.GetTotBytes()
+        zip_bytes = tree.GetZipBytes()
+        summary = {
+            'total_bytes': total_bytes,
+            'total_bytes_s': bytes_to_readable(total_bytes),
+            'zip_bytes': zip_bytes,
+            'zip_bytes_s': bytes_to_readable(zip_bytes),
+            'comp_factor': total_bytes / zip_bytes
+        }
+        return summary
+
+    def get_compression_summary(self, treenames:Optional[List[str]]=None):
+        file = self.obj
+        comp_setting = file.GetCompressionSettings()
+        summary = {}
+        summary["comp_setting"] = comp_setting
+        summary["trees"] = {}
+        if treenames is None:
+            treenames = self._get_all_treenames(file)
+        for treename in treenames:
+            summary["trees"][treename] = self.get_tree_compression_summary(treename)
+        return summary
+        
     @semistaticmethod
-    def fetch_remote_files(self, paths:Union[str, List[str]],
+    def copy_remote_files(self, paths:Union[str, List[str]],
                           cache:bool=True,
                           cachedir:str="/tmp",
+                          parallel:bool=False,
                           **kwargs):
         if isinstance(paths, str):
             paths = [paths]
@@ -199,21 +262,30 @@ class TFile(TObject):
                 self.stdout.warning(f"Remote file {path} can be accessed locally. Skipped.")
                 continue
             remote_paths.append(path)
-        filenames = self.list_files(remote_paths, resolve_cache=cache,
-                                    expand_remote_files=True)
+        from quickstats.interface.xrootd import switch_cachedir
+        with switch_cachedir(cachedir):
+            filenames = self.list_files(remote_paths, resolve_cache=cache,
+                                        expand_remote_files=True)
         cached_files = [filename for filename in filenames if not is_remote_path(filename)]
         files_to_fetch = [filename for filename in filenames if is_remote_path(filename)]
         if cached_files:
             self.stdout.info(f'Cached remote file(s):\n' + '\n'.join(cached_files))
-        from quickstats.interface.xrootd.utils import copy_files
         src, dst = [], []
         for file in files_to_fetch:
             src.append(file)
-            dst.append(self._get_cache_path(file))
-        if src:
-            self.stdout.info(f'Fetching remote file(s):\n' + '\n'.join(src))
-            self.stdout.info(f'Destination(s):\n' + '\n'.join(dst))
-        copy_files(src, dst, force=not cache, **kwargs)
+            dst.append(self._get_cache_path(file, cachedir=cachedir))
+        if not src:
+            return None
+        from quickstats.interface.xrootd import XRDHelper
+        helper = XRDHelper(verbosity=self.stdout.verbosity)
+        if parallel:
+            helper.copy_files(src, dst, force=not cache, **kwargs)
+            return None
+        for src_i, dst_i in zip(src, dst):
+            helper.copy_files([src_i], [dst_i], force=not cache, **kwargs)
+
+    def get(self, key:str):
+        return self.obj.Get(key)
 
     def close(self):
         self.obj.Close()
diff --git a/quickstats/interface/root/__init__.py b/quickstats/interface/root/__init__.py
index 345d2750888e4698777c0962e12e9aac77b0d619..773ff628e1af17dbfbdcb68da96cc68ac7fb5aef 100644
--- a/quickstats/interface/root/__init__.py
+++ b/quickstats/interface/root/__init__.py
@@ -1,5 +1,7 @@
 import quickstats
 
+quickstats.core.methods._require_module("ROOT", quickstats.core.methods.is_root_installed)
+
 from .macros import load_macros, load_macro
 from .TObject import TObject
 from .TArrayData import TArrayData
diff --git a/quickstats/interface/servicex/__init__.py b/quickstats/interface/servicex/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6ab1ed7c8f421b9763edc44b191211e6d544b39c
--- /dev/null
+++ b/quickstats/interface/servicex/__init__.py
@@ -0,0 +1,5 @@
+import quickstats
+
+quickstats.core.methods._require_module("servicex")
+
+from .core import *
\ No newline at end of file
diff --git a/quickstats/interface/servicex/core.py b/quickstats/interface/servicex/core.py
new file mode 100644
index 0000000000000000000000000000000000000000..400478cc0a7ffbbe831ccd320cfc26fda1117445
--- /dev/null
+++ b/quickstats/interface/servicex/core.py
@@ -0,0 +1,47 @@
+from typing import Optional
+from functools import partial
+
+import httpx
+from servicex.configuration import Configuration
+
+read_bak = Configuration.read
+AsyncClient_bak = httpx.AsyncClient
+
+__all__ = ["set_cache_path", "set_async_client_timeout"]
+
+def set_cache_path(cache_path:Optional[str]=None):
+    def overwrite_read(cls, config_path: Optional[str] = None):
+        if config_path:
+            yaml_config = Configuration._add_from_path(Path(config_path), walk_up_tree=False)
+        else:
+            yaml_config = Configuration._add_from_path(walk_up_tree=True)
+
+        if yaml_config:
+            yaml_config['cache_path'] = cache_path
+            return Configuration(**yaml_config)
+        else:
+            path_extra = f"in {config_path}" if config_path else ""
+            raise NameError(
+                "Can't find .servicex or servicex.yaml config file " + path_extra
+            )
+    if cache_path is None:
+        Configuration.read = read_bak
+    else:
+        Configuration.read = classmethod(overwrite_read)
+
+def set_async_client_timeout(timeout:Optional[float]=None,
+                             connect:Optional[float]=None,
+                             read:Optional[float]=None,
+                             write:Optional[float]=None,
+                             pool:Optional[float]=None,):
+    timeout_spec = {}
+    if connect is not None:
+        timeout_spec['connect'] = connect
+    if read is not None:
+        timeout_spec['read'] = read
+    if write is not None:
+        timeout_spec['write'] = write
+    if pool is not None:
+        timeout_spec['pool'] = pool        
+    timeout = httpx.Timeout(timeout, **timeout_spec)
+    httpx.AsyncClient = partial(AsyncClient_bak, timeout=timeout)
\ No newline at end of file
diff --git a/quickstats/interface/xrootd/__init__.py b/quickstats/interface/xrootd/__init__.py
index fa049b6d52d13e277689f065d570d27794cf666c..c44608532b1b57d8bcd70461fe708eff26e1025c 100644
--- a/quickstats/interface/xrootd/__init__.py
+++ b/quickstats/interface/xrootd/__init__.py
@@ -1 +1,7 @@
-from .core import get_cachedir, set_cachedir, switch_cachedir
\ No newline at end of file
+import quickstats
+
+quickstats.core.methods._require_module("XRootD")
+
+from .core import *
+from .filesystem import *
+from .xrd_helper import XRDHelper
\ No newline at end of file
diff --git a/quickstats/interface/xrootd/core.py b/quickstats/interface/xrootd/core.py
index 7c00bda193cd3f534b98fc9f518ccbf2ce2e4208..fe45ef739b9c00a5f566a1d8b654092ff0ba3ba7 100644
--- a/quickstats/interface/xrootd/core.py
+++ b/quickstats/interface/xrootd/core.py
@@ -1,5 +1,7 @@
 from contextlib import contextmanager
 
+__all__ = ["get_cachedir", "set_cachedir", "switch_cachedir"]
+
 class Setting:
     CACHEDIR = None
 
@@ -9,7 +11,6 @@ def get_cachedir():
 def set_cachedir(dirname:str=None):
     Setting.CACHEDIR = dirname
 
-
 @contextmanager
 def switch_cachedir(dirname:str):
     try:
diff --git a/quickstats/interface/xrootd/filesystem.py b/quickstats/interface/xrootd/filesystem.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e9c84aedc49a1b90ca09b68833f2ab5fb13972e
--- /dev/null
+++ b/quickstats/interface/xrootd/filesystem.py
@@ -0,0 +1,148 @@
+from typing import Optional, Union
+import sys
+if sys.version_info[0] > 2:
+    from urllib.parse import urlparse
+else:
+    from urlparse import urlparse
+
+from quickstats import AbstractObject, timer
+from XRootD.client import FileSystem as XRootDFileSystem
+from XRootD.client.flags import StatInfoFlags
+from  XRootD.client import glob_funcs
+
+FILESYSTEMS = {}
+
+__all__ = ["FileSystem", "get_filesystem"]
+
+def split_url(url):
+    parsed_uri = urlparse(url)
+    domain = '{uri.scheme}://{uri.netloc}/'.format(uri=parsed_uri)
+    path = parsed_uri.path
+    if path.startswith("//"):
+        path = path[1:]
+    return domain, path
+
+class FileSystem(AbstractObject):
+
+    def __init__(self, url:str,
+                 verbosity:Optional[Union[int, str]]="INFO"):
+        super().__init__(verbosity=verbosity)
+        self.url = url
+        self.filesystem = XRootDFileSystem(url)
+        self.triggered = False
+        self.sanity_check()
+        
+    def __getattr__(self, name:str):
+        def method(*args, **kwargs):
+            return self._run_query(name, *args, **kwargs)
+        return method
+
+    def _run_query(self, method:str, *args, **kwargs):
+        suppress_error = kwargs.pop("suppress_error", False)
+        if not hasattr(self.filesystem, method):
+            raise ValueError(f'XRootD FileSystem does not contain the method "{method}"')
+        if not self.triggered:
+            self.stdout.info(f'Initializing XRootD query to the server {self.url}. '
+                             f'Network traffic overhead might be expected.')
+        with timer() as t:
+            status, result = getattr(self.filesystem, method)(*args, **kwargs)
+        if not self.triggered:
+            self.stdout.info(f"Query completed in {t.interval:.2f}s")
+            self.triggered = True
+        if not suppress_error:
+            self._process_status(status, method)
+        return status, result
+
+    def _process_status(self, status, name:str):
+        if status.error:
+            self.stdout.warning(f'Query "{name}" responded with error status. Message: {status.message}')
+
+    def sanity_check(self):
+        if "root://" in self.url:
+            from quickstats.interface.kerberos import list_service_principals
+            sercice_principals = list_service_principals()
+            if not any("CERN.CH@CERN.CH" in principal for principal in sercice_principals):
+                self.stdout.warning("No kerberos ticket found for CERN.CH. "
+                                    "XRootD might not work properly. "
+                                    "Available kerberos service principals:")
+                self.stdout.warning("\n".join(sercice_principals), bare=True)
+            else:
+                self.stdout.info("Found valid kerberos ticket for CERN.CH.")
+
+    def copy(self, source:str, target:str, force=False):
+        return self._run_query('copy', source, target, force=force)
+        
+    def listdir(self, path:str, timeout=0, **kwargs):
+        status, result = self._run_query('dirlist', path, timeout=timeout, **kwargs)
+        if status.error:
+            return []
+        return [dir_.name for dir_ in result.dirlist]
+
+    def stat(self, path:str, timeout=0, **kwargs):
+        return self._run_query('stat', path, timeout=timeout, **kwargs)
+
+    def size(self, path:str, timeout=0, **kwargs):
+        status, result = self.stat(path, timeout=timeout, suppress_error=True)
+        if status.error:
+            return None
+        return result.size
+    
+    def exists(self, path:str, timeout=0):
+        status, result = self.stat(path, timeout=timeout, suppress_error=True)
+        return not status.error
+
+    def isdir(self, path:str, timeout=0, **kwargs):
+        status, result = self.stat(path, timeout=timeout, suppress_error=True)
+        return (not status.error) and (result.flags & StatInfoFlags.IS_DIR) != 0
+
+    def isreadable(self, path:str, timeout=0, **kwargs):
+        status, result = self.stat(path, timeout=timeout, suppress_error=True)
+        return (not status.error) and (result.flags & StatInfoFlags.IS_READABLE) != 0
+
+    def iswritable(self, path:str, timeout=0, **kwargs):
+        status, result = self.stat(path, timeout=timeout, suppress_error=True)
+        return (not status.error) and (result.flags & StatInfoFlags.IS_WRITABLE) != 0
+
+    def glob(self, path:str, nourl:bool=True, **kwargs):
+        url = self.url
+        if not url.endswith('/'):
+            url += '/'
+        result =  glob_funcs.glob(url + path)
+        if nourl:
+            return [p.replace(self.url, "").replace("//", "/") for p in result]
+        return result
+
+    def ls(self, path:str, nourl:bool=True, **kwargs):
+        if "*" in path:
+            return self.glob(path, nourl=nourl, **kwargs)
+        timeout = kwargs.get("timeout", 0)
+        status, result = self.stat(path, timeout=timeout)
+        if status.error:
+            return []
+        if result.flags & StatInfoFlags.IS_DIR:
+            return self.listdir(path, **kwargs)
+        return [path]
+        
+    def mv(self, source:str, dest:str, timeout=0, **kwargs):
+        return self._run_query('mv', source, dest, timeout=timeout, **kwargs)
+
+    def rm(self, path:str, timeout=0, **kwargs):
+        return self._run_query('rm', path, timeout=timeout, **kwargs)
+
+    def mkdir(self, path:str, timeout=0, **kwargs):
+        return self._run_query('mkdir', path, timeout=timeout, **kwargs)
+
+    def rmdir(self, path:str, timeout=0, **kwargs):
+        return self._run_query('rmdir', path, timeout=timeout, **kwargs)
+    
+def get_filesystem(url:str):
+    """
+    Parameters: url (string) – The URL of the server to connect with
+    """
+    url = url.rstrip("/")
+    if url in FILESYSTEMS:
+        return FILESYSTEMS[url]
+    filesystem = FileSystem(url)
+    # caching the filesystem instance
+    FILESYSTEMS[url] = filesystem
+    return filesystem
\ No newline at end of file
diff --git a/quickstats/interface/xrootd/path.py b/quickstats/interface/xrootd/path.py
new file mode 100644
index 0000000000000000000000000000000000000000..ebf390d0c42ef927bffbea30c2aef4becfe19c9f
--- /dev/null
+++ b/quickstats/interface/xrootd/path.py
@@ -0,0 +1,35 @@
+from .filesystem import split_url, get_filesystem
+
+def _call_path_method(method:str, path:str, **kwargs):
+    domain, path = split_url(path)
+    filesystem = get_filesystem(domain)
+    if not hasattr(filesystem, method):
+        raise ValueError(f'not implemented method: {method}')
+    return getattr(filesystem, method)(path, **kwargs)
+
+def listdir(path:str, **kwargs):
+    return _call_path_method('listdir', path, **kwargs)
+
+def mkdir(path:str, **kwargs):
+    return _call_path_method('mkdir', path, **kwargs)
+
+def ls(path:str, nourl:bool=False, **kwargs):
+    return _call_path_method('ls', path, nourl=nourl, **kwargs)
+
+def rmdir(path:str, **kwargs):
+    return _call_path_method('rmdir', path, **kwargs)
+
+def rm(path:str, **kwargs):
+    return _call_path_method('rm', path, **kwargs)
+
+def isdir(path:str, **kwargs):
+    return _call_path_method('isdir', path, **kwargs)
+
+def exists(path:str, **kwargs):
+    return _call_path_method('exists', path, **kwargs)
+
+def glob(path:str, nourl:bool=False, **kwargs):
+    return _call_path_method('glob', path, nourl=nourl, **kwargs)
+
+def stat(path:str, **kwargs):
+    return _call_path_method('stat', path, **kwargs)
\ No newline at end of file
diff --git a/quickstats/interface/xrootd/utils.py b/quickstats/interface/xrootd/utils.py
deleted file mode 100644
index a907076855c414087990f71529809b0937cc8e44..0000000000000000000000000000000000000000
--- a/quickstats/interface/xrootd/utils.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from typing import List
-from XRootD.client import CopyProcess
-
-#https://xrootd.slac.stanford.edu/doc/python/xrootd-python-0.1.0/modules/client/copyprocess.html
-def copy_files(src:List[str], dst:List[str], force:bool=False, **kwargs):
-    copy_process = CopyProcess()
-    for src_i, dst_i in zip(src, dst):
-        copy_process.add_job(src_i, dst_i, force=force, **kwargs)
-    copy_process.prepare()
-    copy_process.run()
\ No newline at end of file
diff --git a/quickstats/interface/xrootd/xrd_helper.py b/quickstats/interface/xrootd/xrd_helper.py
new file mode 100644
index 0000000000000000000000000000000000000000..89d5906d71c93462f3378bf017f8b11bbd4b73a9
--- /dev/null
+++ b/quickstats/interface/xrootd/xrd_helper.py
@@ -0,0 +1,61 @@
+import os
+from typing import List, Optional, Union
+from XRootD.client import CopyProcess
+
+from quickstats import AbstractObject, semistaticmethod, timer
+
+class XRDHelper(AbstractObject):
+
+    def __init__(self, verbosity:Optional[Union[int, str]]="INFO"):
+        super().__init__(verbosity=verbosity)
+
+    @staticmethod
+    def get_nbytes(paths:List[str]):
+        pass        
+
+    #https://xrootd.slac.stanford.edu/doc/python/xrootd-python-0.1.0/modules/client/copyprocess.html
+    @semistaticmethod
+    def copy_files(self, src:List[str], dst:List[str], force:bool=False, **kwargs):
+        self.stdout.info(f'Copying remote file(s):\n' + '\n'.join(src))
+        self.stdout.info(f'Destination(s):\n' + '\n'.join(dst))
+        with timer() as t:
+            copy_process = CopyProcess()
+            for src_i, dst_i in zip(src, dst):
+                copy_process.add_job(src_i, dst_i, force=force, **kwargs)
+            copy_process.prepare()
+            copy_process.run()
+        self.stdout.info(f"Copy finished. Total time taken: {t.interval}.")
+
+    @semistaticmethod
+    def copy_file_cli(self, src:str, dst:str, recursive:bool=False,
+                      force:bool=False, allow_http:bool=False, pbar:bool=True,
+                      retry:Optional[int]=None, silent:bool=False):
+        options = []
+        if allow_http:
+            options.append("--allow-http")
+        if force:
+            options.append("--force")
+        if not pbar:
+            options.append("--nopbar")
+        if recursive:
+            options.append("--recursive")
+        if retry is not None:
+            options.append(f"--retry {retry}")
+        if silent:
+            options.append("--silent")
+        options = " ".join(options)
+        cmd = f"xrdcp {options} {src} {dst}"
+        os.system(cmd)
+            
+    @semistaticmethod
+    def copy_files_cli(self, src:List[str], dst:List[str], recursive:bool=False,
+                       force:bool=False, allow_http:bool=False, pbar:bool=True,
+                       retry:Optional[int]=None, silent:bool=False):
+        self.stdout.info(f'Copying remote file(s):\n' + '\n'.join(src))
+        self.stdout.info(f'Destination(s):\n' + '\n'.join(dst))
+        with timer() as t:
+            for src_i, dst_i in zip(src, dst):
+                self.copy_file_cli(src_i, dst_i, recursive=recursive,
+                                   force=force, allow_http=allow_http,
+                                   pbar=pbar, retry=retry, silent=silent)
+        self.stdout.info(f"Copy finished. Total time taken: {t.interval}.")
\ No newline at end of file
diff --git a/quickstats/maths/numerics.py b/quickstats/maths/numerics.py
index 7c07c5c4052338b33afa0007a829c714ca37ff44..0cfb0f377cc9fc5d5337b6527bdc058c5567f58b 100644
--- a/quickstats/maths/numerics.py
+++ b/quickstats/maths/numerics.py
@@ -1,4 +1,4 @@
-from typing import Union, Any, List, Dict, Optional, Tuple
+from typing import Union, Any, List, Dict, Optional, Tuple, Callable
 from fractions import Fraction
 import decimal
 
@@ -202,4 +202,55 @@ def cartesian_product(*arrays):
     arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype)
     for i, a in enumerate(np.ix_(*arrays)):
         arr[...,i] = a
-    return arr.reshape(-1, la)
\ No newline at end of file
+    return arr.reshape(-1, la)
+
+def get_mask(x, conditions:List[Union[Tuple[float, float], Callable]]=None):
+    mask = np.full(x.shape, False)
+    for condition in conditions:
+        if isinstance(condition, (tuple, list)):
+            xmin, xmax = condition
+            mask |= ((x > xmin) & (x < xmax))
+        else:
+            mask |= np.array(list(map(condition, x)))
+    return mask
+
+def get_subsequences(arr, mask, min_length=1):
+    """
+    Finds and returns continuous subsequences of an array where the mask is True.
+    
+    Parameters:
+    - arr (np.array): The array from which to extract subsequences.
+    - mask (np.array): A boolean array where True indicates the elements of `arr` to consider for forming subsequences.
+    - min_length (int): The minimum length of the subsequence to be returned. Default is 2.
+    
+    Returns:
+    - list of np.array: A list containing the subsequences from `arr` that meet the criteria of continuous True values in `mask` and are at least `min_length` elements long.
+    
+    Example:
+    >>> arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+    >>> mask = np.array([False, True, True, False, False, True, True, True, False, True])
+    >>> get_subsequences(arr, mask, min_length=3)
+    [array([6, 7, 8])]
+    """
+    
+    # Ensure mask is a boolean array
+    mask = np.asarray(mask, dtype=bool)
+    
+    # Calculate changes in the mask
+    changes = np.diff(mask.astype(int))
+    # Identify where sequences start (False to True transition)
+    start_indices = np.where(changes == 1)[0] + 1
+    # Identify where sequences end (True to False transition)
+    end_indices = np.where(changes == -1)[0] + 1
+    
+    # Handle case where mask starts with True
+    if mask[0]:
+        start_indices = np.insert(start_indices, 0, 0)
+    # Handle case where mask ends with True
+    if mask[-1]:
+        end_indices = np.append(end_indices, len(mask))
+    
+    # Gather and return sequences that meet the minimum length requirement
+    sequences = [arr[start:end] for start, end in zip(start_indices, end_indices) if end - start >= min_length]
+    
+    return sequences
\ No newline at end of file
diff --git a/quickstats/maths/statistics.py b/quickstats/maths/statistics.py
index 7a24a653ce46a19ffdbc5143ee20f3386596fdc4..cdb2d052f9179c6d727b5d53202bb8b6f6f87262 100644
--- a/quickstats/maths/statistics.py
+++ b/quickstats/maths/statistics.py
@@ -1,4 +1,4 @@
-from typing import Union, Optional, List, Dict, Tuple, Sequence
+from typing import Union, Optional, List, Dict, Tuple, Sequence, Callable
 
 import math
 import numpy as np
@@ -515,8 +515,6 @@ def histogram2d(x:np.ndarray, y:np.ndarray,
             unit_weight = np.allclose(weights, np.ones(weights.shape))
             error_option = BinErrorOption.POISSON if unit_weight else BinErrorOption.SUMW2
         if error_option == BinErrorOption.POISSON:
-            from pdb import set_trace
-            set_trace()
             pois_interval = get_poisson_interval(bin_content.flatten())
             bin_errors =  (pois_interval["lo"].reshape(bin_content.shape),
                            pois_interval["hi"].reshape(bin_content.shape))
@@ -621,6 +619,7 @@ def get_hist_data(x:np.ndarray, weights:Optional[np.ndarray]=None,
     }
     return hist_data
 
+
 def get_stacked_hist_data(x:List[np.ndarray],
                           weights:List[Optional[np.ndarray]]=None,
                           bins:Union[int, Sequence]=10,
@@ -654,12 +653,7 @@ def get_stacked_hist_data(x:List[np.ndarray],
                                   error_option=error_option)
         return hist_data
     else:
-        stacked_hist_data = {
-            "x": [],
-            "y": [],
-            "xerr": [],
-            "yerr": []
-        }
+        hist_data_list = []
         if weights is None:
             weights = len(x) * None
         for x_i, weights_i in zip(x, weights):
@@ -672,18 +666,29 @@ def get_stacked_hist_data(x:List[np.ndarray],
                                       clip_weight=clip_weight,
                                       xerr=xerr, yerr=yerr,
                                       error_option=error_option)
-            stacked_hist_data['x'].append(hist_data['x'])
-            stacked_hist_data['y'].append(hist_data['y'])
-            stacked_hist_data['xerr'].append(hist_data['xerr'])
-            stacked_hist_data['yerr'].append(hist_data['yerr'])
+            hist_data_list.append(hist_data)
         if normalize:
-            norm_factor = np.sum(stacked_hist_data['y'])
-            stacked_hist_data['y'] = [y / norm_factor for y in stacked_hist_data['y']]
+            norm_factor = np.sum([data['y'] for data in hist_data_list])
+            for data in hist_data_list:
+                data['y'] = data['y'] / norm_factor
+                if isinstance(data['yerr'], tuple):
+                    data['yerr'] = (data['yerr'][0] / norm_factor,
+                                    data['yerr'][1] / norm_factor)
+                elif data['yerr'] is not None:
+                    data['yerr'] = data['yerr'] / norm_factor
         if divide_bin_width:
             bin_edges = np.histogram_bin_edges([bin_range[0], bin_range[1]],
                                                bins=bins, range=bin_range)
             bin_widths = bin_edge_to_bin_width(bin_edges)
-            stacked_hist_data['y'] = [y / bin_widths for y in stacked_hist_data['y']]
+            for data in hist_data_list:
+                data['y'] = data['y'] / bin_widths
+                if isinstance(data['yerr'], tuple):
+                    data['yerr'] = (data['yerr'][0] / bin_widths,
+                                    data['yerr'][1] / bin_widths)
+                elif data['yerr'] is not None:
+                    data['yerr'] = data['yerr'] / bin_widths
+        from quickstats.utils.common_utils import list_of_dict_to_dict_of_list
+        stacked_hist_data = list_of_dict_to_dict_of_list(hist_data_list)
         return stacked_hist_data
 
 def get_sumw2(weights:np.ndarray):
@@ -731,6 +736,17 @@ def get_bin_centers_from_range(xlow:float, xhigh:float, nbins:int, bin_precision
     bins = np.around(np.linspace(low_bin_center, high_bin_center, nbins), bin_precision)
     return bins
 
+def select_binned_data(mask, x, y, xerr=None, yerr=None):
+    x, y = x[mask], y[mask]
+    def select_err(err, mask_):
+        if (err is None) or (not isinstance(err, (list, tuple, np.ndarray))):
+            return err
+        if isinstance(err, tuple):
+            return (select_err(err[0], mask_), select_err(err[1], mask_))
+        return err[mask_]
+    xerr, yerr = select_err(xerr, mask), select_err(yerr, mask)
+    return x, y, xerr, yerr    
+
 def pvalue_to_significance(pvalue:float):
     import ROOT
     significance = ROOT.RooStats.PValueToSignificance(pvalue)
diff --git a/quickstats/plots/abstract_plot.py b/quickstats/plots/abstract_plot.py
index d03d78162a9cec3d153d379974b0e88812068a43..ba6bc3cae10a55036799842441e3f36dcd1e5aed 100644
--- a/quickstats/plots/abstract_plot.py
+++ b/quickstats/plots/abstract_plot.py
@@ -8,12 +8,15 @@ import matplotlib
 from quickstats import AbstractObject, semistaticmethod
 from quickstats.plots import get_color_cycle, get_cmap
 from quickstats.plots.color_schemes import QUICKSTATS_PALETTES
-from quickstats.plots.template import (single_frame, parse_styles, format_axis_ticks,
+from quickstats.plots.template import (single_frame, ratio_frame,
+                                       parse_styles, format_axis_ticks,
                                        parse_analysis_label_options, centralize_axis,
-                                       create_transform, draw_multiline_text)
+                                       create_transform, draw_multiline_text,
+                                       CUSTOM_HANDLER_MAP)
 from quickstats.utils.common_utils import combine_dict, insert_periodic_substr
-from quickstats.maths.statistics import bin_center_to_bin_edge, get_hist_comparison_data
+from quickstats.maths.statistics import bin_center_to_bin_edge, get_hist_comparison_data, select_binned_data
 from quickstats.maths.statistics import HistComparisonMode
+from quickstats.maths.numerics import get_mask, get_subsequences
 from .core import PlotFormat, ErrorDisplayFormat
 
 class AbstractPlot(AbstractObject):
@@ -112,6 +115,8 @@ class AbstractPlot(AbstractObject):
             return self.resolve_handle_label(handle[0])
         elif isinstance(handle, tuple):
             _, label = self.resolve_handle_label(handle[0])
+            handle = tuple([h[0] if (isinstance(h, list) and len(h) == 1) \
+                            else h for h in handle])
         elif hasattr(handle, 'get_label'):
             label = handle.get_label()
         else:
@@ -180,8 +185,10 @@ class AbstractPlot(AbstractObject):
                         labels.append(label)
         return handles, labels
     
-    def draw_frame(self, frame_method:Callable=None, **kwargs):
-        if frame_method is None:
+    def draw_frame(self, ratio:bool=False, **kwargs):
+        if ratio:
+            frame_method = ratio_frame
+        else:
             frame_method = single_frame
         ax = frame_method(styles=self.styles,
                           prop_cycle=get_color_cycle(self.cmap),
@@ -319,6 +326,153 @@ class AbstractPlot(AbstractObject):
             elif mode == HistComparisonMode.DIFFERENCE:
                 ylabel = "Difference"
         self.draw_axis_components(ax, xlabel=xlabel, ylabel=ylabel)
+
+
+        if plot_format == PlotFormat.HIST:
+            # draw data
+            hist_y, _, handle = ax.hist(hist_data['x'], bins, range=bin_range,
+                                        weights=hist_data['y'], **styles)
+            assert np.allclose(hist_data['y'], hist_y)
+            # draw error only
+            handles = self.draw_binned_data(ax, hist_data,
+                                            bin_edges=bin_edges,
+                                            draw_data=False,
+                                            draw_error=target_show_error,
+                                            error_format=error_format,
+                                            error_styles=error_styles)
+            if not isinstance(handle, list):
+                handle = [handle]
+            handles = tuple(list(handles) + handle)
+        elif plot_format == PlotFormat.ERRORBAR:
+            handles = self.draw_binned_data(ax, hist_data,
+                                            bin_edges=bin_edges,
+                                            styles=styles,
+                                            draw_error=target_show_error,
+                                            error_format=error_format,
+                                            error_styles=error_styles)
+
+    def _draw_hist_from_binned_data(self, ax, x, y,
+                                    xerr=None, yerr=None,
+                                    bin_edges:Optional[np.ndarray]=None,
+                                    hide:Optional[Union[Tuple[float, float], Callable]]=None,
+                                    styles:Optional[Dict]=None):
+        styles = combine_dict(self.styles['hist'], styles)
+        # assume uniform binning
+        if bin_edges is None:
+            bin_center = np.array(x)
+            bin_edges = bin_center_to_bin_edge(bin_center)
+        bins, range = bin_edges, (bin_edges[0], bin_edges[1])
+        if hide is not None:
+            mask = get_mask(x, [hide])
+            y[mask] = 0.
+        hist_y, _, handle = ax.hist(x, bins, range=range,
+                                    weights=y, **styles)
+        assert np.allclose(y, hist_y)
+        return handle
+
+    def _draw_stacked_hist_from_binned_data(self, ax, x_list, y_list,
+                                            bin_edges:Optional[np.ndarray]=None,
+                                            hide_list:Optional[List[Union[Tuple[float, float], Callable]]]=None,
+                                            styles:Optional[Dict]=None):
+        styles = combine_dict(self.styles['hist'], styles)
+        if bin_edges is None:
+            bin_edges_list = []
+            for x in x_list:
+                bin_center = np.array(x)
+                bin_edges = bin_center_to_bin_edge(bin_center)
+                bin_edges_list.append(bin_edges)
+        
+            if not all(np.array_equal(bin_edges, bin_edges_list[0]) \
+                       for bin_edges in bin_edges_list):
+                raise RuntimeError('subhistograms in a stacked histogram have different binnings')
+            bin_edges = bin_edges_list[0]
+        bins, range = bin_edges, (bin_edges[0], bin_edges[1])
+        if hide_list is not None:
+            assert len(hide_list) == len(x_list)
+            for i, hide in enumerate(hide_list):
+                if hide is None:
+                    continue
+                mask = get_mask(x_list[i], [hide])
+                y_list[i][mask] = 0.
+        hist_y, _, handles = ax.hist(x_list, bins, range=range,
+                                     weights=y_list, stacked=True,
+                                     **styles)
+        #assert ...
+        return hist_y, handles
+        
+    def _draw_errorbar(self, ax, x, y,
+                       xerr=None, yerr=None,
+                       hide:Optional[Union[Tuple[float, float], Callable]]=None,
+                       styles:Optional[Dict]=None):
+        styles = combine_dict(self.styles['errorbar'], styles)
+        if hide is not None:
+            mask = ~get_mask(x, [hide])
+            x, y, xerr, yerr = select_binned_data(mask, x, y, xerr, yerr)            
+        handle = ax.errorbar(x, y, xerr=xerr, yerr=yerr, **styles)
+        return handle
+
+    def _draw_fill_from_binned_data(self, ax, x, y,
+                                    xerr=None, yerr=None,
+                                    bin_edges:Optional[np.ndarray]=None,
+                                    hide:Optional[Union[Tuple[float, float], Callable]]=None,
+                                    styles:Optional[Dict]=None):
+        styles = combine_dict(self.styles['fill_between'], styles)
+        # assume uniform binning
+        if bin_edges is None:
+            bin_center = np.array(x)
+            bin_edges = bin_center_to_bin_edge(bin_center)
+        if yerr is None:
+            yerr = 0.
+        indices = np.arange(len(x))
+        if hide is not None:
+            mask = ~get_mask(x, [hide])
+            sections_indices = get_subsequences(indices, mask, min_length=2)
+        else:
+            sections_indices = [indices]
+        handles = []
+        for section_indices in sections_indices:
+            mask = np.full(x.shape, False)
+            mask[section_indices] = True
+            x_i, y_i, xerr_i, yerr_i = select_binned_data(mask, x, y, xerr, yerr)
+            # extend to edge
+            x_i[0] = bin_edges[section_indices[0]]
+            x_i[-1] = bin_edges[section_indices[-1] + 1]
+            if isinstance(yerr_i, tuple):
+                yerrlo = y_i - yerr_i[0]
+                yerrhi = y_i + yerr_i[1]
+            else:
+                yerrlo = y_i - yerr_i
+                yerrhi = y_i + yerr_i
+            handle = ax.fill_between(x_i, yerrlo, yerrhi, **styles)
+            handles.append(handle)
+        return handles[0]
+
+    def _draw_shade_from_binned_data(self, ax, x, y,
+                                     xerr=None, yerr=None,
+                                     bin_edges:Optional[np.ndarray]=None,
+                                     hide:Optional[Union[Tuple[float, float], Callable]]=None,
+                                     styles:Optional[Dict]=None):
+        styles = combine_dict(styles)
+        # assume uniform binning
+        if bin_edges is None:
+            bin_center = np.array(x)
+            bin_edges = bin_center_to_bin_edge(bin_center)
+        bin_widths = np.diff(bin_edges)
+        if yerr is None:
+            yerr = 0.
+        if hide is not None:
+            mask = ~get_mask(x, [hide])
+            x, y, xerr, yerr = select_binned_data(mask, x, y, xerr, yerr)
+            bin_widths = bin_widths[mask]
+        if isinstance(yerr, tuple):
+            height = yerr[0] + yerr[1]
+            bottom = y - yerr[0]
+        else:
+            height = 2 * yerr
+            bottom = y - yerr
+        handle = ax.bar(x=x, height=height, bottom=bottom,
+                        width=bin_widths, **styles)
+        return handle
         
     def draw_binned_data(self, ax, data,
                          draw_data:bool=True,
@@ -327,12 +481,15 @@ class AbstractPlot(AbstractObject):
                          plot_format:Union[PlotFormat, str]='errorbar',
                          error_format:Union[ErrorDisplayFormat, str]='errorbar',
                          styles:Optional[Dict]=None,
+                         hide:Optional[Union[Tuple[float, float], Callable]]=None,
                          error_styles:Optional[Dict]=None):
+        if (not draw_data) and (not draw_error):
+            raise ValueError('can not draw nothing')
         if styles is None:
             styles = {}
         if error_styles is None:
             error_styles = {}
-        plot_format = PlotFormat.parse(plot_format)
+        plot_format  = PlotFormat.parse(plot_format)
         error_format = ErrorDisplayFormat.parse(error_format)
         handle, error_handle = None, None
         
@@ -340,36 +497,125 @@ class AbstractPlot(AbstractObject):
         xerr, yerr = data.get('xerr', 0), data.get('yerr', 0)
         
         if draw_data:
-            styles = combine_dict(self.styles['errorbar'], styles)
-            if plot_format == PlotFormat.ERRORBAR:
+            if plot_format == PlotFormat.HIST:
+                handle = self._draw_hist_from_binned_data(ax, x, y,
+                                                          bin_edges=bin_edges,
+                                                          hide=hide,
+                                                          styles=styles)
+            elif plot_format == PlotFormat.ERRORBAR:
                 if (not draw_error) or (error_format != ErrorDisplayFormat.ERRORBAR):
-                    handle = ax.errorbar(x, y, **styles)
+                    handle = self._draw_errorbar(ax, x, y,
+                                                 hide=hide,
+                                                 styles=styles)
                 else:
-                    handle = ax.errorbar(**data, **styles)
+                    handle = self._draw_errorbar(ax, x, y,
+                                                 xerr=xerr, yerr=yerr,
+                                                 hide=hide,
+                                                 styles=styles)
             else:
                 raise RuntimeError(f'unsupported plot format: {plot_format.name}')
                 
         if draw_error:
             if error_format == ErrorDisplayFormat.FILL:
-                if isinstance(yerr, tuple):
-                    error_handle = ax.fill_between(x, y - yerr[0], y + yerr[1],
-                                                   **error_styles, zorder=-1)
-                else:
-                    error_handle = ax.fill_between(x, y - yerr, y + yerr,
-                                                   **error_styles, zorder=-1)
+                error_handle = self._draw_fill_from_binned_data(ax, x, y, yerr=yerr,
+                                                                hide=hide,
+                                                                styles={**error_styles,
+                                                                        "zorder": -1})
             elif error_format == ErrorDisplayFormat.SHADE:
-                if bin_edges is None:
-                    bin_edges = bin_center_to_bin_edge(x)
-                bin_widths = np.diff(bin_edges)
-                if isinstance(yerr, tuple):
-                    error_handle = ax.bar(x=x, height=yerr[0] + yerr[1],
-                                          bottom=y - yerr[0], width=bin_widths,
-                                          **error_styles, zorder=-1)
-                else:
-                    error_handle = ax.bar(x=x, height=2*yerr,
-                                          bottom=y - yerr, width=bin_widths,
-                                          **error_styles, zorder=-1)
-            elif error_format == ErrorDisplayFormat.ERRORBAR:
-                error_handle = ax.errorbar(**data, **error_styles)
+                error_handle = self._draw_shade_from_binned_data(ax, x, y, yerr=yerr,
+                                                                 bin_edges=bin_edges,
+                                                                 hide=hide,
+                                                                 styles={**error_styles,
+                                                                        "zorder": -1})
+            elif ((error_format == ErrorDisplayFormat.ERRORBAR) and
+                 ((not draw_data) or (plot_format !=PlotFormat.ERRORBAR))):
+                error_handle = self._draw_errorbar(ax, x, y,
+                                                   xerr=xerr, yerr=yerr,
+                                                   hide=hide,
+                                                   styles={**error_styles,
+                                                           "marker": 'none'})
+        if isinstance(handle, list):
+            handle = handle[0]
         handles = tuple([h for h in [handle, error_handle] if h is not None])
-        return handles
\ No newline at end of file
+        return handles
+
+    def draw_stacked_binned_data(self, ax, data,
+                                 draw_data:bool=True,
+                                 draw_error:bool=True,
+                                 bin_edges:Optional[np.ndarray]=None,
+                                 plot_format:Union[PlotFormat, str]='errorbar',
+                                 error_format_list:Union[ErrorDisplayFormat, str]='errorbar',
+                                 styles:Optional[Dict]=None,
+                                 hide_list:Optional[Union[Tuple[float, float], Callable]]=None,
+                                 error_styles_list:Optional[Dict]=None):
+        if (not draw_data) and (not draw_error):
+            raise ValueError('can not draw nothing')
+        n_component = len(data['x'])
+        if styles is None:
+            styles = {}
+        if error_styles_list is None:
+            error_styles_list = [{}] * n_component
+        plot_format  = PlotFormat.parse(plot_format)
+        error_format_list = [ErrorDisplayFormat.parse(fmt) for fmt in error_format_list]
+        handles, error_handles = None, None
+        
+        x_list, y_list = data['x'], data['y']
+        xerr_list, yerr_list = data.get('xerr', None), data.get('yerr', None)
+        if draw_data:
+            if plot_format == PlotFormat.HIST:
+                plot_func = self._draw_stacked_hist_from_binned_data
+                hist_y, handles = plot_func(ax, x_list, y_list,
+                                            bin_edges=bin_edges,
+                                            hide_list=hide_list,
+                                            styles=styles)
+            else:
+                raise RuntimeError(f'unsupported format for stacked plot: {plot_format.name}')
+        if draw_error:
+            error_handles = []
+            def get_component(obj, index):
+                if obj is not None:
+                    return obj[index]
+                return None
+            for i in range(n_component):
+                error_format = error_format_list[i]
+                x, y = x_list[i], hist_y[i]
+                xerr = get_component(xerr_list, i)
+                yerr = get_component(yerr_list, i)
+                hide = get_component(hide_list, i)
+                error_styles = get_component(error_styles_list, i)
+                if error_format == ErrorDisplayFormat.FILL:
+                    error_handle = self._draw_fill_from_binned_data(ax, x, y, yerr=yerr,
+                                                                    hide=hide,
+                                                                    styles={**error_styles,
+                                                                            "zorder": -1})
+                elif error_format == ErrorDisplayFormat.SHADE:
+                    error_handle = self._draw_shade_from_binned_data(ax, x, y, yerr=yerr,
+                                                                     bin_edges=bin_edges,
+                                                                     hide=hide,
+                                                                     styles={**error_styles,
+                                                                            "zorder": -1})
+                elif error_format == ErrorDisplayFormat.ERRORBAR:
+                    error_handle = self._draw_errorbar(ax, x, y,
+                                                       xerr=xerr, yerr=yerr,
+                                                       hide=hide,
+                                                       styles={**error_styles,
+                                                               "marker": 'none'})
+                error_handles.append(error_handle)
+        if error_handles is None:
+            return handles
+        if handles is None:
+            return error_handles
+        handles = [(handle, error_handle) for handle, error_handle in zip(handles, error_handles)]
+        return handles    
+
+    def draw_legend(self, ax, handles=None, labels=None,
+                    handler_map=None, **kwargs):
+        if (handles is None) and (labels is None):
+            handles, labels = self.get_legend_handles_labels()
+        if handler_map is not None:
+            handler_map = {**CUSTOM_HANDLER_MAP, **handler_map}
+        else:
+            handler_map = CUSTOM_HANDLER_MAP
+        styles = {**self.styles['legend'], **kwargs}
+        styles['handler_map'] = handler_map
+        ax.legend(handles, labels, **styles)
\ No newline at end of file
diff --git a/quickstats/plots/bidirectional_bar_chart.py b/quickstats/plots/bidirectional_bar_chart.py
index 51033904b2722f348ea66538bb375b8b321f4bce..ee53a887d3caba521726e4bedea37b0f3aebc548 100644
--- a/quickstats/plots/bidirectional_bar_chart.py
+++ b/quickstats/plots/bidirectional_bar_chart.py
@@ -264,7 +264,7 @@ class BidirectionalBarChart(AbstractPlot):
             secondary_styles['color'] = 'k'
             target_handles = [lines.Line2D([0], [0], **primary_styles),
                               lines.Line2D([0], [0], **secondary_styles)]
-            ax.legend(target_handles, target_labels, **self.styles['legend'])
+            self.draw_legend(ax, handles=target_handles, labels=target_labels)
             ax.add_artist(legend_updown)
         """
         # legend for targets
diff --git a/quickstats/plots/general_1D_plot.py b/quickstats/plots/general_1D_plot.py
index c886ed921237f16872153dc64364133ccf7b9965..2633d6dc167758de1254f06606057a8784d5bd4c 100644
--- a/quickstats/plots/general_1D_plot.py
+++ b/quickstats/plots/general_1D_plot.py
@@ -167,8 +167,7 @@ class General1DPlot(AbstractPlot):
             raise ValueError("invalid data format")
             
         self.legend_order = legend_order
-        handles, labels = self.get_legend_handles_labels()
-        ax.legend(handles, labels, **self.styles['legend'])
+        self.draw_legend(ax)
         
         self.draw_axis_components(ax, xlabel=xlabel, ylabel=ylabel)
         self.set_axis_range(ax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, ypad=ypad)
diff --git a/quickstats/plots/general_2D_plot.py b/quickstats/plots/general_2D_plot.py
index ea6799c31f247a36bce4e6ee8cd2efd7853de3c9..d1ca2c4a786a3db174fd46d65aae291826c0e068 100644
--- a/quickstats/plots/general_2D_plot.py
+++ b/quickstats/plots/general_2D_plot.py
@@ -100,11 +100,12 @@ class General2DPlot(AbstractPlot):
              
             if draw_clabel:
                 ax.clabel(handle, **self.styles['clabel'])
+                
         if draw_contourf:
             handle = ax.contourf(X, Y, Z, levels=contour_levels, **self.styles['contourf'])
+            
         if draw_scatter:
             handle = ax.scatter(x, y, **self.styles['scatter'])
-            ax.legend(**self.styles['legend'])
              
         self.draw_axis_components(ax, xlabel=xlabel, ylabel=ylabel,
                                   title=title)
diff --git a/quickstats/plots/general_distribution_plot.py b/quickstats/plots/general_distribution_plot.py
index 6e348daf9fc47c79bfd5a820af40a5089ce609ee..e55493de6f0e3d34cf7b5950eaf0f055f3003168 100644
--- a/quickstats/plots/general_distribution_plot.py
+++ b/quickstats/plots/general_distribution_plot.py
@@ -9,7 +9,7 @@ from quickstast import semistaticmethod
 from quickstats.plots.color_schemes import QUICKSTATS_PALETTES
 
 from quickstats.plots import AbstractPlot, CollectiveDataPlot
-from quickstats.plots.template import suggest_markersize, ratio_frames, centralize_axis, create_transform
+from quickstats.plots.template import suggest_markersize, centralize_axis, create_transform
 from quickstats.utils.common_utils import combine_dict
 from quickstats import GeneralEnum
 
@@ -38,7 +38,7 @@ class GeneralDistributionPlot(AbstractPlot):
         "legend": {
             "borderpad": 1
         },
-        "ratio_frames": {
+        "ratio_frame": {
             "height_ratios": (4, 1)
         }
     }
@@ -384,10 +384,9 @@ class GeneralDistributionPlot(AbstractPlot):
              ypad:Optional[float]=None):
         
         if comparison_options is not None:
-            ax, ax_ratio = self.draw_frame(ratio_frames, logx=logx, logy=logy,
-                                           **self.styles["ratio_frames"])
+            ax, ax_ratio = self.draw_frame(ratio=True, logx=logx, logy=logy)
         else:
-            ax = self.draw_frame(logx=logx, logy=logy)
+            ax = self.draw_frame(ratio=False, logx=logx, logy=logy)
 
         for name in self.collective_data:
             if (targets is not None) and(name not in targets):
@@ -426,9 +425,8 @@ class GeneralDistributionPlot(AbstractPlot):
                     self.colors[name] = handle[0].get_color()
         
         self.draw_axis_components(ax, xlabel=xlabel, ylabel=ylabel)
-        self.set_axis_range(ax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, ypad=ypad)  
-        handles, labels = self.get_legend_handles_labels()
-        ax.legend(handles, labels, **self.styles['legend'])
+        self.set_axis_range(ax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, ypad=ypad)
+        self.draw_legend(ax)
         
         if comparison_options is not None:
             if not isinstance(comparison_options, list):
diff --git a/quickstats/plots/hypotest_inverter_plot.py b/quickstats/plots/hypotest_inverter_plot.py
index a8123ec5b49c18755786a41ce63c70b34ed531d9..faa0598d88474969f868338ff939f2eaef1879b3 100644
--- a/quickstats/plots/hypotest_inverter_plot.py
+++ b/quickstats/plots/hypotest_inverter_plot.py
@@ -150,5 +150,5 @@ class HypoTestInverterPlot(AbstractPlot):
             handles[2].set_linewidth(1.0)
             handles = handles[3:] + [handles[0], handles[1], (handles[2], border_leg)]
             labels = labels[3:] + [labels[0], labels[1], labels[2]]
-        ax.legend(handles, labels, loc='upper right', frameon=False, **self.styles['legend'])
+        self.draw_legend(ax, handles, labels, loc=loc, frameon=frameon)
         return ax
\ No newline at end of file
diff --git a/quickstats/plots/likelihood_2D_plot.py b/quickstats/plots/likelihood_2D_plot.py
index f9635948f796caffab329c3c7cbeef13775b08ec..db4e61097462b9ef35d31fddbe047c63d4de6935 100644
--- a/quickstats/plots/likelihood_2D_plot.py
+++ b/quickstats/plots/likelihood_2D_plot.py
@@ -363,8 +363,7 @@ class Likelihood2DPlot(AbstractPlot):
                       **self.config['sm_line_styles'])
 
         if draw_legend:
-            handles, labels = self.get_legend_handles_labels()
-            ax.legend(handles, labels, **self.styles['legend'])
+            self.draw_legend(ax)
 
         self.draw_axis_components(ax, xlabel=xlabel, ylabel=ylabel)
         self.set_axis_range(ax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax)
diff --git a/quickstats/plots/likelihood_scan_plot.py b/quickstats/plots/likelihood_scan_plot.py
deleted file mode 100644
index 652b5b3c84d158ff708eecee3e0edc054463b7ea..0000000000000000000000000000000000000000
--- a/quickstats/plots/likelihood_scan_plot.py
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-class LikelihoodScanPlot(object):
-    def __init__(self):
-        self._data = None
-        
-    @property
-    def data(self):
-        return self._data
-    
-    def load(self, input_files):
-        pass
-             
\ No newline at end of file
diff --git a/quickstats/plots/pdf_distribution_plot.py b/quickstats/plots/pdf_distribution_plot.py
index 0d56b32898ae75b13c256d561b457447cfa16665..d79963745aea9a440ffc8fa6ada28d8d148ae050 100644
--- a/quickstats/plots/pdf_distribution_plot.py
+++ b/quickstats/plots/pdf_distribution_plot.py
@@ -7,7 +7,7 @@ import numpy as np
 from quickstats.plots.color_schemes import QUICKSTATS_PALETTES
 
 from quickstats.plots import AbstractPlot
-from quickstats.plots.template import suggest_markersize, ratio_frames, centralize_axis, create_transform
+from quickstats.plots.template import suggest_markersize, centralize_axis, create_transform
 from quickstats.utils.common_utils import combine_dict
 from quickstats import GeneralEnum
 
@@ -37,7 +37,7 @@ class PdfDistributionPlot(AbstractPlot):
         "legend": {
             "borderpad": 1
         },
-        "ratio_frames": {
+        "ratio_frame": {
             "height_ratios": (4, 1)
         }
     }
@@ -409,10 +409,9 @@ class PdfDistributionPlot(AbstractPlot):
              ypad:Optional[float]=None):
         
         if comparison_options is not None:
-            ax, ax_ratio = self.draw_frame(ratio_frames, logx=logx, logy=logy,
-                                           **self.styles["ratio_frames"])
+            ax, ax_ratio = self.draw_frame(ratio=True, logx=logx, logy=logy)
         else:
-            ax = self.draw_frame(logx=logx, logy=logy)
+            ax = self.draw_frame(ratio=False, logx=logx, logy=logy)
         
         if targets is None:
             targets = list(self.collective_data)
@@ -457,8 +456,7 @@ class PdfDistributionPlot(AbstractPlot):
 
         self.draw_axis_components(ax, xlabel=xlabel, ylabel=ylabel)
         self.set_axis_range(ax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, ypad=ypad)  
-        handles, labels = self.get_legend_handles_labels()
-        ax.legend(handles, labels, **self.styles['legend'])
+        self.draw_legend(ax)
         
         if comparison_options is not None:
             if not isinstance(comparison_options, list):
diff --git a/quickstats/plots/sample_purity_plot.py b/quickstats/plots/sample_purity_plot.py
index 083a561dab9e0579c6616c2116940a111411a44f..0184b8f1a319427a7e616d0981e8228710cc1177 100644
--- a/quickstats/plots/sample_purity_plot.py
+++ b/quickstats/plots/sample_purity_plot.py
@@ -120,10 +120,13 @@ class SamplePurityPlot(AbstractPlot):
                         continue
                     ax.text(x, y, to_string(c, precision), ha='center', va='center',
                             color=text_color)
-        legend_styles = combine_dict(self.styles['legend'])
-        if ('ncol' in legend_styles) and (legend_styles['ncol'] == 'auto'):
-            legend_styles['ncol'] = len(categories)
-        ax.legend(**legend_styles)
+        if (('ncol' in self.styles['legend']) and
+            (self.styles['legend']['ncol'] == 'auto')):
+            legend_styles = {"ncol": len(categories)}
+        else:
+            legend_styles = {}
+        
+        self.draw_legend(ax, **legend_styles)
         
         return ax
     
\ No newline at end of file
diff --git a/quickstats/plots/score_distribution_plot.py b/quickstats/plots/score_distribution_plot.py
index 08ebec66d32f7561be1dfa89e98e3260792348ea..ff7de668bd81f128881b73991c07196e16cdd8f1 100644
--- a/quickstats/plots/score_distribution_plot.py
+++ b/quickstats/plots/score_distribution_plot.py
@@ -117,7 +117,7 @@ class ScoreDistributionPlot(AbstractPlot):
         handles, labels = ax.get_legend_handles_labels()
         new_handles = [Line2D([], [], c=h.get_edgecolor(), linestyle=h.get_linestyle(),
                               **self.styles['legend_Line2D']) if isinstance(h, Polygon) else h for h in handles]
-        ax.legend(handles=new_handles, labels=labels, **self.styles['legend'])
+        self.draw_legend(ax, handles=new_handles, labels=labels)
         if boundaries is not None:
             for boundary in boundaries:
                 ax.axvline(x=boundary, **self.config["boundary_style"])
@@ -157,7 +157,7 @@ def score_distribution_plot(dfs:Dict[str, pd.DataFrame], hist_options:Dict[str,
     handles, labels = ax.get_legend_handles_labels()
     new_handles = [Line2D([], [], c=h.get_edgecolor(), linestyle=h.get_linestyle(), **styles['legend_Line2D'])
                    if isinstance(h, Polygon) else h for h in handles]
-    ax.legend(handles=new_handles, labels=labels, **styles['legend'])
+    self.draw_legend(ax, new_handles, labels)
     if boundaries is not None:
         for boundary in boundaries:
             ax.axvline(x=boundary, ymin=0, ymax=0.5, linestyle='--', color='k')
diff --git a/quickstats/plots/template.py b/quickstats/plots/template.py
index 0be787dadec38a85848c812ef12be83759adfbd8..7b7afbaf50702de33c4f5a6ad0f637d0467e995e 100644
--- a/quickstats/plots/template.py
+++ b/quickstats/plots/template.py
@@ -8,14 +8,16 @@ import numpy as np
 import matplotlib.pyplot as plt
 import matplotlib.transforms as transforms
 from matplotlib.patches import Rectangle, Polygon
-from matplotlib.collections import PolyCollection
+from matplotlib.collections import (PolyCollection, LineCollection, PathCollection)
 from matplotlib.lines import Line2D
 from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,
                                AutoMinorLocator, ScalarFormatter,
                                Locator, Formatter, AutoLocator,
                                LogFormatter, LogFormatterSciNotation,
                                MaxNLocator)
-
+from matplotlib.legend_handler import (HandlerLine2D,
+                                       HandlerLineCollection,
+                                       HandlerPathCollection)
 from quickstats.utils.common_utils import combine_dict
 from quickstats import DescriptiveEnum
 
@@ -67,6 +69,26 @@ class LogNumericFormatter(LogFormatterSciNotation):
         result = super().__call__(x, pos)
         #result = result.replace('10^{1}', '10').replace('10^{0}', '1')
         return result
+        
+class CustomHandlerLineCollection(HandlerLineCollection):
+    def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans):
+        artists = super().create_artists(legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans)
+        # Adjust line height to center in legend
+        for artist in artists:
+            artist.set_ydata([height / 2.0, height / 2.0])
+        return artists
+
+class CustomHandlerPathCollection(HandlerPathCollection):
+    def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans):
+        artists = super().create_artists(legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans)
+        # Modify the path collection offsets to center the markers in the legend
+        for artist in artists:
+            offsets = np.array([[width / 2.0, height / 2.0]])
+            artist.set_offsets(offsets)
+        return artists
+
+CUSTOM_HANDLER_MAP = {LineCollection: CustomHandlerLineCollection(),
+                      PathCollection: CustomHandlerPathCollection()}
 
 TEMPLATE_STYLES = {
     'default': {
@@ -166,7 +188,7 @@ TEMPLATE_STYLES = {
             "fontsize": 20,
             "columnspacing": 0.8
         },
-        'ratio_frames':{
+        'ratio_frame':{
             'height_ratios': (3, 1),
             'hspace': 0.07            
         },
@@ -244,44 +266,43 @@ def parse_analysis_label_options(options:Optional[Dict]=None):
         options = combine_dict(default_options, options)
     return options
 
-def ratio_frames(height_ratios:Tuple[int]=(3, 1), hspace:float=0.07,
-                 logx:bool=False, logy:bool=False, 
-                 styles:Optional[Union[Dict, str]]=None,
-                 analysis_label_options:Optional[Union[Dict, str]]=None,
-                 prop_cycle:Optional[List[str]]=None,
-                 figure_index:Optional[int]=None):
+def ratio_frame(logx:bool=False, logy:bool=False, 
+                styles:Optional[Union[Dict, str]]=None,
+                analysis_label_options:Optional[Union[Dict, str]]=None,
+                prop_cycle:Optional[List[str]]=None,
+                figure_index:Optional[int]=None):
     if figure_index is None:
         plt.clf()
     else:
         plt.figure(figure_index)
     styles = parse_styles(styles)
     gridspec_kw = {
-        "height_ratios": height_ratios,
-        "hspace": hspace
+        "height_ratios": styles['ratio_frame']['height_ratios'],
+        "hspace": styles['ratio_frame']['hspace']
     }
-    fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1, gridspec_kw=gridspec_kw,
-                                   sharex=True, **styles['figure'])
+    fig, (ax_main, ax_ratio) = plt.subplots(nrows=2, ncols=1, gridspec_kw=gridspec_kw,
+                                            sharex=True, **styles['figure'])
     
     if logx:
-        ax1.set_xscale('log')
-        ax2.set_xscale('log')
+        ax_main.set_xscale('log')
+        ax_ratio.set_xscale('log')
         
     if logy:
-        ax1.set_yscale('log')
+        ax_main.set_yscale('log')
     
-    ax1_styles = combine_dict(styles['axis'], {"x_axis_styles": {"labelbottom": False}})
-    format_axis_ticks(ax1, x_axis=True, y_axis=True, xtick_styles=styles['xtick'],
-                      ytick_styles=styles['ytick'], **ax1_styles)
-    format_axis_ticks(ax2, x_axis=True, y_axis=True, xtick_styles=styles['xtick'],
+    ax_main_styles = combine_dict(styles['axis'], {"x_axis_styles": {"labelbottom": False}})
+    format_axis_ticks(ax_main, x_axis=True, y_axis=True, xtick_styles=styles['xtick'],
+                      ytick_styles=styles['ytick'], **ax_main_styles)
+    format_axis_ticks(ax_ratio, x_axis=True, y_axis=True, xtick_styles=styles['xtick'],
                       ytick_styles=styles['ytick'], **styles['axis'])
     
     if analysis_label_options is not None:
         draw_analysis_label(ax1, text_options=styles['text'], **analysis_label_options)
         
     if prop_cycle is not None:
-        ax1.set_prop_cycle(prop_cycle)
+        ax_main.set_prop_cycle(prop_cycle)
     
-    return ax1, ax2
+    return ax_main, ax_ratio
 
 def single_frame(logx:bool=False, logy:bool=False, 
                  styles:Optional[Union[Dict, str]]=None,
@@ -665,7 +686,7 @@ def change_axis(axis):
 def draw_analysis_label(axis, loc=(0.05, 0.95), fontsize:float=25, status:str='int',
                         energy:Optional[str]=None, lumi:Optional[str]=None,
                         colab:Optional[str]='ATLAS', main_text:Optional[str]=None,
-                        extra_text:Optional[str]=None, dy:float=0.05, dy_main:float=0.02,
+                        extra_text:Optional[str]=None, dy:float=0.02, dy_main:float=0.01,
                         transform_x:str='axis', transform_y:str='axis',
                         vertical_align:str='top', horizontal_align:str='left',
                         text_options:Optional[Dict]=None):
diff --git a/quickstats/plots/test_statistic_distribution_plot.py b/quickstats/plots/test_statistic_distribution_plot.py
index 76ea705727d3d8b4f77410b8e8d91d0ebad9a17e..958ba30d953c3c260da107c27e011aeed3e20460 100644
--- a/quickstats/plots/test_statistic_distribution_plot.py
+++ b/quickstats/plots/test_statistic_distribution_plot.py
@@ -171,14 +171,16 @@ class TestStatisticDistributionPlot(AbstractPlot):
         if asymptotic_handles is not None:
             primary_handles += asymptotic_handles
         primary_labels = [labels[handles.index(h)] for h in primary_handles]
-        primary_leg = ax.legend(primary_handles, primary_labels, 
-                                loc=leg_loc, ncol=2, **self.styles['legend'])
+        primary_leg = self.draw_legend(ax, handles=primary_handles,
+                                       labels=primary_labels,
+                                       loc=leg_loc, ncol=2)
         ax.add_artist(primary_leg)
         
         if secondary_handles is not None:
             secondary_labels = [labels[handles.index(h)] for h in secondary_handles]
-            primary_leg = ax.legend(secondary_handles, secondary_labels, 
-                                    loc=leg_loc, ncol=2, **self.styles['legend'])
+            primary_leg = self.draw_legend(ax, handles=secondary_handles,
+                                           labels=secondary_labels, 
+                                           loc=leg_loc, ncol=2)
             ax.add_artist(primary_leg)
         
         
diff --git a/quickstats/plots/upper_limit_1D_plot.py b/quickstats/plots/upper_limit_1D_plot.py
index 0117ca528bceb49a171d33ba33e9bb09667bf8b8..7ff1eac99348fb2263e0bfa5fe85fa4b03d1afd0 100644
--- a/quickstats/plots/upper_limit_1D_plot.py
+++ b/quickstats/plots/upper_limit_1D_plot.py
@@ -126,11 +126,9 @@ class UpperLimit1DPlot(AbstractPlot):
             text_pos = {'expected': 0.925}
         if draw_third_column:
             text_pos = {'observed': 0.725, 'expected': 0.825, 'third': 0.925}
-
-        bak_verticalalignment = self.styles['text']['verticalalignment']
-        bak_horizontalalignment = self.styles['text']['horizontalalignment']
-        self.styles['text']['verticalalignment'] = 'center'
-        self.styles['text']['horizontalalignment'] = 'center'
+        text_styles = self.styles['text'].copy()
+        text_styles['verticalalignment']   = 'center'
+        text_styles['horizontalalignment'] = 'center'
         for i, category in enumerate(self.category_df):
             df = self.category_df[category]
             # draw observed
@@ -143,8 +141,7 @@ class UpperLimit1DPlot(AbstractPlot):
                 observed_handle = (handle_1, handle_2)
                 if add_text:
                     ax.text(text_pos['observed'], i + 0.5, f"{{:.{sig_fig}f}}".format(observed_limit),
-                            transform=transform,
-                            **self.styles['text'])
+                            transform=transform, **text_styles)
             else:
                 observed_handle = None
             # draw stat
@@ -152,16 +149,14 @@ class UpperLimit1DPlot(AbstractPlot):
                 stat_limit = df['stat']
                 if add_text:
                     ax.text(text_pos['stat'], i + 0.5, f"({{:.{sig_fig}f}})".format(stat_limit),
-                            transform=transform,
-                            **self.styles['text'])
+                            transform=transform, **text_styles)
             # draw expected
             expected_limit = df['0']
             expected_handle = ax.vlines(expected_limit, i, i + 1, colors=self.color_pallete['expected'], linestyles='dotted',
                                         zorder=1.1, label=self.labels['expected'])
             if add_text:
                 ax.text(text_pos['expected'], i + 0.5, f"{{:.{sig_fig}f}}".format(expected_limit),
-                        transform=transform,
-                        **self.styles['text'])
+                        transform=transform, **text_styles)
             # draw third
             if draw_third_column:
                 third_limit = df['third']
@@ -169,8 +164,7 @@ class UpperLimit1DPlot(AbstractPlot):
                                          zorder=1.1, label=self.labels['third'])
                 if add_text:
                     ax.text(text_pos['third'], i + 0.5, f"{{:.{sig_fig}f}}".format(third_limit),
-                            transform=transform,
-                            **self.styles['text'])
+                            transform=transform, **text_styles)
             else:
                 third_handle = None
             # draw error band
@@ -212,21 +206,15 @@ class UpperLimit1DPlot(AbstractPlot):
         if add_text:
             if draw_observed:
                 ax.text(text_pos['observed'], n_category + 0.3, 'Obs.',
-                        transform=transform,
-                        **self.styles['text'])
+                        transform=transform, **text_styles)
             if draw_stat:
                 ax.text(text_pos['stat'], n_category + 0.3, '(Stat.)',
-                        transform=transform,
-                        **self.styles['text'])
+                        transform=transform, **text_styles)
             if draw_third_column:
                 ax.text(text_pos['third'], n_category + 0.3, draw_third_column,
-                        transform=transform,
-                        **self.styles['text'])
+                        transform=transform, **text_styles)
             ax.text(text_pos['expected'], n_category + 0.3, 'Exp.',
-                    transform=transform,
-                    **self.styles['text'])
-        self.styles['text']['verticalalignment'] = bak_verticalalignment
-        self.styles['text']['horizontalalignment'] = bak_horizontalalignment
+                    transform=transform, **text_styles)
         if self.curve_data is not None:
             self.draw_curve(ax, self.curve_data)
         if xlabel is not None:
@@ -234,6 +222,5 @@ class UpperLimit1DPlot(AbstractPlot):
         # border for the legend
         border_leg = patches.Rectangle( (0, 0), 1, 1, facecolor='none', edgecolor='black', linewidth=1)
         self.add_legend_decoration(border_leg, targets=["one_sigma", "two_sigma", "curve"])
-        handles, labels = self.get_legend_handles_labels()
-        ax.legend(handles, labels, **self.styles['legend'])
+        self.draw_legend(ax)
         return ax
diff --git a/quickstats/plots/upper_limit_2D_plot.py b/quickstats/plots/upper_limit_2D_plot.py
index e69775ad68d3746b9db4ec675cbec50329145afb..bf278289c1acd9328fecf79623b51cce5692f055 100644
--- a/quickstats/plots/upper_limit_2D_plot.py
+++ b/quickstats/plots/upper_limit_2D_plot.py
@@ -458,6 +458,6 @@ class UpperLimit2DPlot(AbstractPlot):
 
         indices = sorted(self.legend_data_ext.keys())
         handles, labels = self.get_legend_handles_labels(idx=indices)
-        ax.legend(handles, labels, **self.styles['legend'])
+        self.draw_legend(ax, handles, labels)
 
         return ax
diff --git a/quickstats/plots/upper_limit_2D_plot_deprecated.py b/quickstats/plots/upper_limit_2D_plot_deprecated.py
deleted file mode 100644
index e351337097c7d565778838edc1fee308e9f70f8f..0000000000000000000000000000000000000000
--- a/quickstats/plots/upper_limit_2D_plot_deprecated.py
+++ /dev/null
@@ -1,309 +0,0 @@
-from typing import Optional, Union, Dict, List
-
-import matplotlib.patches as patches
-import matplotlib.lines as lines
-import pandas as pd
-
-from quickstats.plots import AbstractPlot
-from quickstats.utils.common_utils import combine_dict
-
-class UpperLimit2DPlot(AbstractPlot):
-    
-    STYLES = {
-        'axis':{
-            'tick_bothsides': False
-        },
-        'errorbar': {
-            "linewidth": 1,
-            "markersize": 5,
-            "marker": 'o',
-        }
-    }
-    
-    COLOR_PALLETE = {
-        '2sigma': '#FDC536',
-        '1sigma': '#4AD9D9',
-        'expected': 'k',
-        'observed': 'k'
-    }
-    
-    COLOR_PALLETE_EXTRA = {
-        '2sigma': '#FDC536',
-        '1sigma': '#4AD9D9',
-        'expected': 'r',
-        'observed': 'r'
-    }
-    
-    LABELS = {
-        '2sigma': 'Expected limit $\pm 2\sigma$',
-        '1sigma': 'Expected limit $\pm 1\sigma$',
-        'expected': 'Expected limit (95% CL)',
-        'observed': 'Observed limit (95% CL)'
-    }
-    
-    LABELS_EXTRA = {
-        '2sigma': 'Expected limit $\pm 2\sigma$',
-        '1sigma': 'Expected limit $\pm 1\sigma$',
-        'expected': 'Expected limit (95% CL)',
-        'observed': 'Observed limit (95% CL)'
-    }
-
-    CONFIG = {
-        'primary_hatch'  : '\\\\\\',
-        'secondary_hatch': '///',
-        'primary_alpha'  : 0.9,
-        'secondary_alpha': 0.8,
-        'curve_line_styles': {
-            'color': 'darkred' 
-        },
-        'curve_fill_styles':{
-            'color': 'hh:darkpink'
-        },
-        'highlight_styles': {
-            'linewidth' : 0,
-            'marker' : '*',
-            'markersize' : 20,
-            'color' : '#E9F1DF',
-            'markeredgecolor' : 'black'
-        },
-        'errorband_plot_styles':{
-            'alpha': 1
-        },
-        'expected_plot_styles': {
-            'marker': 'None',
-            'linestyle': '--',
-            'alpha': 1,
-            'linewidth': 1
-        },
-        'observed_plot_styles': {
-            'marker': 'o',
-            'alpha': 1,
-            'linewidth': 1
-        }
-    }
-    
-    def __init__(self, data:pd.DataFrame,
-                 additional_data:Optional[List[Dict]]=None,
-                 scale_factor:float=None,
-                 color_pallete:Optional[Dict]=None,
-                 labels:Optional[Dict]=None,
-                 styles:Optional[Union[Dict, str]]=None,
-                 analysis_label_options:Optional[Union[Dict, str]]='default',
-                 config:Optional[Dict]=None):
-        super().__init__(color_pallete=color_pallete,
-                         styles=styles,
-                         analysis_label_options=analysis_label_options,
-                         config=config)
-        self.data     = data
-        
-        self.additional_data = []
-        if additional_data is not None:
-            for _data in additional_data:
-                self.add_data(**_data)
-        
-        self.labels = combine_dict(self.LABELS, labels)
- 
-        self.scale_factor = scale_factor
-        
-        self.curve_data     = None
-        self.highlight_data = None
-        
-    def get_default_legend_order(self):
-        return ['observed', 'expected', '1sigma', '2sigma', 'curve', 'highlight']
-    
-    def add_curve(self, x, y, yerrlo=None, yerrhi=None,
-                  label:str="Theory prediction",
-                  line_styles:Optional[Dict]=None,
-                  fill_styles:Optional[Dict]=None):
-        curve_data = {
-            'x'     : x,
-            'y'     : y,
-            'yerrlo'  : yerrlo,
-            'yerrhi'  : yerrhi,
-            'label' : label,
-            'line_styles': line_styles,
-            'fill_styles': fill_styles,
-        }
-        self.curve_data = curve_data
-                        
-    def add_highlight(self, x:float, y:float, label:str="SM prediction",
-                      styles:Optional[Dict]=None):
-        highlight_data = {
-            'x'     : x,
-            'y'     : y,
-            'label' : label,
-            'styles': styles
-        }
-        self.highlight_data = highlight_data        
-    
-    def draw_curve(self, ax, data):
-        line_styles = data['line_styles']
-        fill_styles = data['fill_styles']
-        if line_styles is None:
-            line_styles = self.config['curve_line_styles']
-        if fill_styles is None:
-            fill_styles = self.config['curve_fill_styles']
-        if (data['yerrlo'] is None) and (data['yerrhi'] is None):
-            line_styles['color'] = fill_styles['color']
-        handle_line = ax.plot(data['x'], data['y'], label=data['label'], **line_styles)
-        handles = handle_line[0]
-        if (data['yerrlo'] is not None) and (data['yerrhi'] is not None):
-            handle_fill = ax.fill_between(data['x'], data['yerrlo'], data['yerrhi'],
-                                          label=data['label'], **fill_styles)
-            handles = (handle_fill, handle_line[0])
-        self.update_legend_handles({'curve': handles}, idx=0)
-        
-    def draw_highlight(self, ax, data):
-        styles = data['styles']
-        if styles is None:
-            styles = self.config['highlight_styles']
-        handle = ax.plot(data['x'], data['y'], label=data['label'], **styles)
-        self.update_legend_handles({'highlight': handle[0]}, idx=0)
-        
-    def draw_single_data(self, ax, data, scale_factor=None,
-                         log:bool=False, 
-                         draw_expected:bool=True,
-                         draw_observed:bool=True,
-                         color_pallete:Optional[Dict]=None,
-                         labels:Optional[Dict]=None,
-                         sigma_band_hatch:Optional[str]=None,
-                         draw_errorband:bool=True,
-                         idx:int=0):
-        
-        if color_pallete is None:
-            color_pallete = self.color_pallete
-        if labels is None:
-            labels = self.labels
-        if scale_factor is None:
-            scale_factor = 1.0
-            
-        indices = data.index.astype(float).values
-        exp_limits = data['0'].values * scale_factor
-        n1sigma_limits = data['-1'].values * scale_factor
-        n2sigma_limits = data['-2'].values * scale_factor
-        p1sigma_limits = data['1'].values * scale_factor
-        p2sigma_limits = data['2'].values * scale_factor
-        
-        handles_map = {}
-        
-        # draw +- 1, 2 sigma bands 
-        if draw_errorband:
-            handle_2sigma = ax.fill_between(indices, n2sigma_limits, p2sigma_limits, 
-                                            facecolor=color_pallete['2sigma'],
-                                            label=labels['2sigma'],
-                                            hatch=sigma_band_hatch,
-                                            **self.config["errorband_plot_styles"])
-            handle_1sigma = ax.fill_between(indices, n1sigma_limits, p1sigma_limits, 
-                                            facecolor=color_pallete['1sigma'],
-                                            label=labels['1sigma'],
-                                            hatch=sigma_band_hatch,
-                                            **self.config["errorband_plot_styles"])
-            handles_map['1sigma'] = handle_1sigma
-            handles_map['2sigma'] = handle_2sigma
-        
-        if log:
-            draw_fn = ax.semilogy
-        else:
-            draw_fn = ax.plot
- 
-        if draw_observed:
-            obs_limits = data['obs'].values * scale_factor
-            handle_observed = draw_fn(indices, obs_limits, color=color_pallete['observed'], 
-                                      label=labels['observed'], 
-                                      **self.config["observed_plot_styles"])
-            handles_map['observed'] = handle_observed[0]
-        
-        if draw_expected:
-            handle_expected = draw_fn(indices, exp_limits, color=color_pallete['expected'],
-                                      label=labels['expected'],
-                                      **self.config["expected_plot_styles"])
-            handles_map['expected'] = handle_expected[0]
-
-        self.update_legend_handles(handles_map, idx=idx)
-        
-    def add_data(self, data:pd.DataFrame, color_pallete:Optional[Dict]=None,
-                 labels:Optional[Dict]=None, draw_expected:bool=True,
-                 draw_observed:bool=False,
-                 draw_errorband:bool=False):
-        config = {
-            "data": data,
-            "color_pallete": combine_dict(self.COLOR_PALLETE_EXTRA, color_pallete),
-            "labels": combine_dict(self.LABELS_EXTRA, labels),
-            "draw_observed": draw_observed,
-            "draw_expected": draw_expected,
-            "draw_errorband": draw_errorband
-        }
-        self.additional_data.append(config)
-            
-    def draw(self, xlabel:str="", ylabel:str="", ylim=None, xlim=None,
-             log:bool=False, draw_expected:bool=True,
-             draw_observed:bool=True, draw_errorband:bool=True,
-             draw_sec_errorband:bool=False, draw_hatch:bool=True):
-        
-        ax = self.draw_frame()
-        
-        if len(self.additional_data) > 0:
-            if draw_hatch:
-                sigma_band_hatch = self.config['secondary_hatch']
-                alpha = self.config['secondary_alpha']
-            else:
-                sigma_band_hatch = None
-                alpha = 1.
-            for idx, config in enumerate(self.additional_data):
-                self.draw_single_data(ax, config["data"],
-                                      scale_factor=self.scale_factor,
-                                      log=log,
-                                      draw_expected=config["draw_expected"],
-                                      draw_observed=config["draw_observed"],
-                                      color_pallete=config["color_pallete"],
-                                      labels=config["labels"],
-                                      sigma_band_hatch=sigma_band_hatch,
-                                      draw_errorband=config["draw_errorband"],
-                                      idx=idx + 1)
-            if draw_hatch:
-                sigma_band_hatch = self.config['primary_hatch']
-                alpha = self.config['primary_alpha']
-            else:
-                sigma_band_hatch = None
-                alpha = 1.
-        else:
-            sigma_band_hatch = None
-            alpha = 1.
-        self.draw_single_data(ax, self.data,
-                              scale_factor=self.scale_factor,
-                              log=log,
-                              draw_expected=draw_expected,
-                              draw_observed=draw_observed,
-                              color_pallete=self.color_pallete,
-                              labels=self.labels,
-                              sigma_band_hatch=sigma_band_hatch,
-                              draw_errorband=draw_errorband,
-                              idx=0)
-        
-        if self.curve_data is not None:
-            self.draw_curve(ax, self.curve_data)   
-        if self.highlight_data is not None:
-            self.draw_highlight(ax, self.highlight_data)
-            
-        self.draw_axis_components(ax, xlabel=xlabel, ylabel=ylabel)
-        
-        if ylim is not None:
-            ax.set_ylim(*ylim)
-        if xlim is not None:
-            ax.set_xlim(*xlim)
-
-        # border for the legend
-        border_leg = patches.Rectangle((0, 0), 1, 1, facecolor = 'none', edgecolor = 'black', linewidth = 1)
-        for legend_data in self.legend_data_ext.values():
-            for sigma in ['1sigma', '2sigma']:
-                if sigma in legend_data:
-                    legend_data[sigma]['handle'] = (legend_data[sigma]['handle'], border_leg)        
-        
-        if self.curve_data is not None:
-            if isinstance(self.legend_data_ext[0]['curve']['handle'], tuple):
-                self.legend_data_ext[0]['curve']['handle'] = (*self.legend_data_ext[0]['curve']['handle'], border_leg)
-        
-        indices = sorted(self.legend_data_ext.keys())
-        handles, labels = self.get_legend_handles_labels(idx=indices)
-        ax.legend(handles, labels, **self.styles['legend'])
-        return ax
diff --git a/quickstats/plots/upper_limit_3D_plot.py b/quickstats/plots/upper_limit_3D_plot.py
index 738a3c397bf10486548bb9b023b43e7298b64475..431f7f6ae71ad9d64725c9cb60ccdf375716e076 100644
--- a/quickstats/plots/upper_limit_3D_plot.py
+++ b/quickstats/plots/upper_limit_3D_plot.py
@@ -291,7 +291,7 @@ class UpperLimit3DPlot(AbstractPlot):
             handles_sec, labels_sec = self.get_legend_handles_labels(sec=True)
             handles = handles + handles_sec
             labels  = labels + labels_sec
-        ax.legend(handles, labels, **self.styles['legend'])
+        self.draw_legend(ax, handles, labels)
 
         return ax
 
diff --git a/quickstats/plots/upper_limit_benchmark_plot.py b/quickstats/plots/upper_limit_benchmark_plot.py
index 82358b35537f326913ec2d857db85d8b56f1cc05..e192473c351ede6dbbb7b6e2c5faf4c07460eff4 100644
--- a/quickstats/plots/upper_limit_benchmark_plot.py
+++ b/quickstats/plots/upper_limit_benchmark_plot.py
@@ -7,7 +7,7 @@ from matplotlib.patches import Polygon
 import numpy as np
 import pandas as pd
 
-from quickstats.plots.template import single_frame, ratio_frames, create_transform, remake_handles
+from quickstats.plots.template import create_transform, remake_handles
 from quickstats.plots import AbstractPlot
 from quickstats.utils.common_utils import combine_dict
 from quickstats.maths.statistics import HistComparisonMode
@@ -463,10 +463,9 @@ class UpperLimitBenchmarkPlot(AbstractPlot):
             xticklabels = list(xticklabels)
 
         if comparison_options is not None:
-            ax, ax_ratio = self.draw_frame(ratio_frames, logy=logy,
-                                           **self.styles["ratio_frames"])
+            ax, ax_ratio = self.draw_frame(ratio=True, logy=logy)
         else:
-            ax = self.draw_frame(logy=logy)
+            ax = self.draw_frame(ratio=False, logy=logy)
             
         eps = self.config['sigma_width'] / 2
         xmargin = self.config['xmargin']
@@ -525,7 +524,7 @@ class UpperLimitBenchmarkPlot(AbstractPlot):
         handles = remake_handles(handles, polygon_to_line=False, fill_border=True,
                                  border_styles=self.styles['legend_border'])
         handler_map = {ErrorbarContainer: HandlerErrorbar(xerr_size=1)}
-        ax.legend(handles, labels, **self.styles['legend'], handler_map=handler_map)
+        self.darw_legend(handles, labels, handler_map=handler_map)
         
         if comparison_options is not None:
             return ax, ax_ratio
diff --git a/quickstats/plots/variable_distribution_plot.py b/quickstats/plots/variable_distribution_plot.py
index 9e37c90123dd83b8463fbb46b8d33e42956e3645..dc0e8b7938c5f1722f8975baad6dca185dbbdb05 100644
--- a/quickstats/plots/variable_distribution_plot.py
+++ b/quickstats/plots/variable_distribution_plot.py
@@ -1,4 +1,4 @@
-from typing import Optional, Union, Dict, List, Sequence
+from typing import Optional, Union, Dict, List, Sequence, Tuple, Callable
 
 import pandas as pd
 import numpy as np
@@ -8,8 +8,8 @@ from matplotlib.lines import Line2D
 from matplotlib.patches import Polygon
 
 from quickstats.plots import AbstractPlot, get_color_cycle
-from quickstats.plots.template import ratio_frames, centralize_axis, remake_handles
-from quickstats.utils.common_utils import combine_dict
+from quickstats.plots.template import centralize_axis, remake_handles
+from quickstats.utils.common_utils import combine_dict, remove_duplicates
 from quickstats.maths.numerics import safe_div
 from quickstats.maths.statistics import (HistComparisonMode,
                                          min_max_to_range, get_hist_data,
@@ -58,11 +58,10 @@ class VariableDistributionPlot(AbstractPlot):
         },
         'plot_format': 'hist',
         'error_format': 'shade',
-        'error_label_format': r'{label} $\pm \sigma$',
+        'error_label_format': r'{label}',
         'show_xerr': False,
         'stacked_label': ':stacked_{index}:',
-        'box_legend_handle': False,
-        'save_hist_data': False
+        'box_legend_handle': False
     }
     
     def __init__(self, data_map:Union["pandas.DataFrame", Dict[str, "pandas.DataFrame"]],
@@ -91,27 +90,37 @@ class VariableDistributionPlot(AbstractPlot):
                     "error_styles": <options in mpl.bar>,
                     "plot_format": "hist" or "errorbar",
                     "show_error": True or False,
-                    "stack_index": <stack index>
+                    "stack_index": <stack index>,
+                    "hide": <list of callables / 2-tuples>
                   }
                 }
-                where "styles" should match the options available in mpl.hist if
-                `plot_format` = "hist" or mpl.errorbar if `plot_format` = "errorbar"
                 
-                (optional) "weight_scale" is used to scale the weights of the given
-                group of samples by the given factor
+                "styles" should match the options available in mpl.hist if
+                `plot_format` = "hist" or mpl.errorbar if `plot_format` = "errorbar"
                 
                 "error_styles" should match the options available in mpl.errorbar if
                 `error_format` = "errorbar", mpl.bar if `error_format` = "shade" or
                 mpl.fill_between if `error_format` = "fill"
+
+                (optional) "weight_scale" is used to scale the weights of the given
+                group of samples by the given factor
                 
                 (optional) "show_error" is used to specify whether to show the errorbar/
                 errorbands for this particular target.
                 
-                (optional) "stack_index" is be used when multiple stacked plots are made;
+                (optional) "stack_index" is used when multiple stacked plots are made;
                 sample groups with the same stack index will be stacked; this option
                 is only used when `plot_format` = "hist" and the draw method is called
                 with the `stack` option set to True; by default a stack index of 0 will
                 be assigned
+
+                (optional) "hide" defines the condition to hide portion of the data in 
+                the plot; in case of a 2-tuple, it specifies the (start, end) range of
+                data that should be hidden; in case of a callable, it is a function
+                that takes as input the value of the variable, and outputs a boolean
+                value indicating whether the data should be hidden;
+
+                Note: If "samples" is not given, it will default to [<sample_group>]
                 
                 Note: If both `plot_format` and `error_format` are errorbar, "styles"
                 will be used instead of "error_styles" for the error styles.
@@ -133,6 +142,11 @@ class VariableDistributionPlot(AbstractPlot):
             data_map = {None: data_map}
         self.data_map = data_map
         
+    def reset_hist_data(self):
+        self.hist_data = {}
+        self.hist_bin_edges = {}
+        self.hist_comparison_data = []
+        
     def set_plot_format(self, plot_format:str):
         self.config['plot_format'] = PlotFormat.parse(plot_format)
         
@@ -141,33 +155,48 @@ class VariableDistributionPlot(AbstractPlot):
         
     def is_single_data(self):
         return (None in self.data_map) and (len(self.data_map) == 1)
-    
-    def resolve_plot_options(self, plot_options:Optional[Dict]=None,
-                             targets:Optional[List[str]]=None):
+
+    def resolve_targets(self, targets:Optional[List[str]]=None,
+                        plot_options:Optional[Dict]=None):
         if self.is_single_data():
             if targets is not None:
-                raise ValueError('no targets should be specified if only a single set of input data is given')
+                raise ValueError('no targets should be specified if only one set of input data is given')
             targets = [None]
         elif targets is None:
+            all_samples = list(self.data_map.keys())
+            targets = []
             if plot_options is not None:
-                targets = list(plot_options.keys())
-            elif isinstance(self.data_map, dict):
-                targets = list(self.data_map.keys())
-        final_plot_options = {}
-        plot_colors = self.get_colors()
-        n_colors, color_i = len(plot_colors), 0
-        if plot_options is None:
-            plot_options = {}
+                grouped_samples = []
+                for key in plot_options:
+                    samples = plot_options[key].get("samples", [key])
+                    grouped_samples.extend([sample for sample in samples \
+                                            if sample not in grouped_samples])
+                    targets.append(key)
+                targets.extend([sample for sample in all_samples \
+                                if sample not in grouped_samples])
+            else:
+                targets = all_samples
+        return targets
+    
+    def resolve_plot_options(self, plot_options:Optional[Dict]=None,
+                             targets:Optional[List[str]]=None):
+        plot_options = plot_options or self.plot_options or {}
+        targets = self.resolve_targets(targets, plot_options=plot_options)
+        resolved_plot_options = {}
+        colors = self.get_colors()
+        n_colors, color_i = len(colors), 0
         if self.label_map is not None:
             label_map = self.label_map
         else:
             label_map = {}
         for target in targets:
             options = combine_dict(plot_options.get(target, {}))
+            # use global plot format if not specified
             if 'plot_format' not in options:
                 options['plot_format'] = PlotFormat.parse(self.config['plot_format'])
             else:
                 options['plot_format'] = PlotFormat.parse(options['plot_format'])
+            # use global error format if not specified
             if 'error_format' not in options:
                 if options['plot_format'] == PlotFormat.ERRORBAR:
                     options['error_format'] = ErrorDisplayFormat.ERRORBAR
@@ -183,11 +212,12 @@ class VariableDistributionPlot(AbstractPlot):
             if 'color' not in options['styles']:
                 if color_i == n_colors:
                     self.stdout.warning("Number of targets is more than the number of colors "
-                                        "available in the color map. The colors will be repeated.")
-                options['styles']['color'] = plot_colors[color_i % n_colors]
+                                        "available in the color map. The colors will be recycled.")
+                options['styles']['color'] = colors[color_i % n_colors]
                 color_i += 1
             if 'label' not in options['styles']:
                 label = label_map.get(target, target)
+                # handle case of single data (no label needed)
                 if label is None:
                     label = 'None'
                 options['styles']['label'] = label
@@ -197,6 +227,7 @@ class VariableDistributionPlot(AbstractPlot):
                 options['error_styles'] = combine_dict(self.get_styles(options['error_format'].mpl_method))
             else:
                 options['error_styles'] = combine_dict(self.get_styles(options['error_format'].mpl_method), options['error_styles'])
+            # reuse color of the plot for the error by default
             if 'color' not in options['error_styles']:
                 options['error_styles']['color'] = options['styles']['color']
             if 'label' not in options['error_styles']:
@@ -206,8 +237,70 @@ class VariableDistributionPlot(AbstractPlot):
                 options['stack_index'] = 0
             if 'weight_scale' not in options:
                 options['weight_scale'] = None
-            final_plot_options[target] = options
-        return final_plot_options
+            if 'hide' not in options:
+                options['hide'] = None
+            resolved_plot_options[target] = options
+        return resolved_plot_options
+
+    def _merge_styles(self, styles_list:List[Dict]):
+        merged_styles = {}
+        sequence_args = ["color", "label"]
+        for styles in styles_list:
+            styles = styles.copy()
+            for key, value in styles.items():
+                if key in sequence_args:
+                    if key not in merged_styles:
+                        merged_styles[key] = []
+                    merged_styles[key].append(value)
+                    continue
+                if (key in merged_styles) and (value != merged_styles[key]):
+                    raise ValueError('failed to merge style options for targets in a stacked plot: '
+                                     f'found inconsistent values for the option "{key}"')
+                merged_styles[key] = value
+        return merged_styles
+
+    def resolve_stacked_plot_options(self, plot_options:Dict):
+        stacked_plot_options = {}
+        targets = [target for target, options in plot_options.items() if \
+                   options['plot_format'] == PlotFormat.HIST]
+        if not targets:
+            raise RuntimeError('no histograms to be stacked')
+        target_map = {}
+        for target in targets:
+            stack_index = plot_options[target]['stack_index']
+            if stack_index not in target_map:
+                target_map[stack_index] = []
+            target_map[stack_index].append(target)
+        stacked_plot_options = {}
+        for stack_index, targets in target_map.items():
+            options = {}
+            options["components"] = {}
+            styles_list = []
+            hide_list = []
+            error_styles_list = []
+            error_format_list = []
+            for target in targets:
+                # modify the dictionary (intended)
+                target_options = plot_options.pop(target)
+                styles = target_options.pop("styles")
+                error_styles = target_options.pop("error_styles")
+                error_format = target_options.pop("error_format")
+                hide = target_options.pop("hide")
+                styles_list.append(styles)
+                error_styles_list.append(error_styles)
+                error_format_list.append(error_format)
+                hide_list.append(hide)
+                options["components"][target] = target_options
+            options['plot_format'] = PlotFormat.HIST
+            options['error_format_list'] = error_format_list
+            options['styles'] = self._merge_styles(styles_list)
+            options['error_styles_list'] = error_styles_list
+            options['hide_list'] = hide_list
+            label = self.config["stacked_label"].format(index=stack_index)
+            if label in stacked_plot_options:
+                raise RuntimeError(f"duplicated stack label: {label}")
+            stacked_plot_options[label] = options
+        return stacked_plot_options
     
     def resolve_comparison_options(self, comparison_options:Optional[Dict]=None,
                                    plot_options:Optional[Dict]=None):
@@ -249,7 +342,7 @@ class VariableDistributionPlot(AbstractPlot):
                 if target in plot_options:
                     component['styles']['color'] = plot_options[target]['styles']['color']
                 else:
-                    component['styles']['color'] = plot_colors[color_i % n_colors]
+                    component['styles']['color'] = colors[color_i % n_colors]
                     color_i += 1
             if 'color' not in component['error_styles']:
                 if target in plot_options:
@@ -289,13 +382,14 @@ class VariableDistributionPlot(AbstractPlot):
             ylim[1] = np.max(y)
         ax.set_ylim(ylim)
 
-        if self.config['save_hist_data']:
-            self.hist_comparison_data.append(comparison_data)
+        self.hist_comparison_data.append(comparison_data)
             
         return handle, error_handle
     
     def deduce_bin_range(self, samples:List[str], column_name:str,
                          variable_scale:Optional[float]=None):
+        """Deduce bin range based on variable ranges from multiple samples
+        """
         xmin = None
         xmax = None
         for sample in samples:
@@ -330,78 +424,102 @@ class VariableDistributionPlot(AbstractPlot):
         if weight_scale is not None:
             weights = weights * weight_scale            
         return x, weights
-    
-    def draw_stacked(self, ax, plot_options:Dict,
-                     column_name:str, weight_name:Optional[str]=None,
-                     bins:Union[int, Sequence]=25,
-                     bin_range:Optional[Sequence]=None,
-                     clip_weight:bool=False,
-                     underflow:bool=False,
-                     overflow:bool=False,
-                     divide_bin_width:bool=False,
-                     normalize:bool=True,
-                     show_error:bool=False,
-                     variable_scale:Optional[float]=None):
+
+    def draw_stacked_target(self, ax, stack_target:str,
+                            components:Dict,
+                            column_name:str,
+                            plot_format:Union[PlotFormat, str],
+                            error_format_list:List[Union[ErrorDisplayFormat, str]],                            
+                            hist_options:Dict,
+                            styles:Dict,
+                            error_styles_list:List[Dict],
+                            variable_scale:Optional[float]=None,
+                            weight_name:Optional[str]=None,
+                            show_error:bool=False,
+                            hide_list:Optional[List[Union[Tuple[float, float], Callable]]]=None):
         stacked_data = {
-            'x'       : [],
-            'weights' : [],
-            'color'   : [],
-            'label'   : [],
+            'x' : [],
+            'y' : []
         }
-
-        stacked_styles = []
-        for target, options in plot_options.items():
-            samples, styles = options['samples'], options['styles']
-            label, color = styles['label'], styles['color']
+        
+        for target, options in components.items():
+            samples = options['samples']
             weight_scale = options['weight_scale']
-            x, weights = self.get_sample_data(samples, column_name,
-                                              variable_scale=variable_scale,
-                                              weight_scale=weight_scale,
-                                              weight_name=weight_name)
-            x = get_clipped_data(x, bin_range=bin_range, clip_lower=underflow, clip_upper=overflow)
+            x, y = self.get_sample_data(samples, column_name,
+                                        variable_scale=variable_scale,
+                                        weight_scale=weight_scale,
+                                        weight_name=weight_name)
+            x = get_clipped_data(x,
+                                 bin_range=hist_options["bin_range"],
+                                 clip_lower=hist_options["underflow"],
+                                 clip_upper=hist_options["overflow"])
             stacked_data['x'].append(x)
-            stacked_data['weights'].append(weights)
-            stacked_data['color'].append(color)
-            stacked_data['label'].append(label)
-            stacked_styles.append(styles)
+            stacked_data['y'].append(y)
+
         bin_edges = np.histogram_bin_edges(np.concatenate(stacked_data['x']).flatten(),
-                                           bins=bins, range=bin_range)
-        hist_data = get_stacked_hist_data(stacked_data['x'], stacked_data['weights'],
-                                          underflow=underflow,
-                                          overflow=overflow,
-                                          divide_bin_width=divide_bin_width,
-                                          normalize=normalize,
-                                          bin_range=bin_range, bins=bins,
-                                          clip_weight=clip_weight,
-                                          xerr=show_error and self.config['show_xerr'],
+                                           bins=hist_options["bins"],
+                                           range=hist_options["bin_range"])
+        show_xerr = show_error and self.config['show_xerr']
+        hist_data = get_stacked_hist_data(stacked_data['x'],
+                                          stacked_data['y'],
+                                          xerr=show_xerr,
                                           yerr=show_error,
-                                          error_option='auto')
-        stacked_styles = {k:v for k,v in stacked_styles[0].items() if k not in ['color', 'label']}
-        stacked_data_processed = get_stacked_hist_data(stacked_data['x'], stacked_data['weights'],
-                                                       underflow=underflow,
-                                                       overflow=overflow,
-                                                       divide_bin_width=divide_bin_width,
-                                                       normalize=normalize,
-                                                       bin_range=bin_range, bins=bins,
-                                                       clip_weight=clip_weight,
-                                                       xerr=False,
-                                                       yerr=False,
-                                                       merge=False,
-                                                       error_option='auto')
-        stacked_data['x'] = stacked_data_processed['x']
-        stacked_data['weights'] = stacked_data_processed['y']
-        hist_y, bin_edges_, handle = ax.hist(**stacked_data,
-                                             bins=bins,
-                                             range=bin_range,
-                                             stacked=True,
-                                             **stacked_styles)
-        for i, target in enumerate(plot_options):
-            self.update_legend_handles({target:handle[i]})
-        return bin_edges, hist_data
+                                          error_option='auto',
+                                          **hist_options)
+        stacked_data = get_stacked_hist_data(stacked_data['x'], stacked_data['y'],
+                                             xerr=show_xerr,
+                                             yerr=show_error,
+                                             merge=False,
+                                             **hist_options)
+        handles = self.draw_stacked_binned_data(ax, stacked_data,
+                                                bin_edges=bin_edges,
+                                                plot_format=plot_format,
+                                                error_format_list=error_format_list,
+                                                draw_error=show_error,
+                                                hide_list=hide_list,
+                                                styles=styles,
+                                                error_styles_list=error_styles_list)
+        for i, target in enumerate(components):
+            self.update_legend_handles({target: handles[i]})
+        self.hist_data[stack_target] = hist_data
+        self.hist_bin_edges[stack_target] = bin_edges
+        #self.update_legend_handles({stack_target: handles})
 
-    def reset_hist_data(self):
-        self.hist_data = {}
-        self.hist_comparison_data = []
+    def draw_single_target(self, ax, target:str, samples:List[str],
+                           column_name:str,
+                           styles:Dict, error_styles:Dict,
+                           plot_format:Union[PlotFormat, str],
+                           error_format:Union[ErrorDisplayFormat, str],
+                           hist_options:Dict,
+                           variable_scale:Optional[float]=None,
+                           weight_name:Optional[str]=None,
+                           weight_scale:Optional[float]=None,
+                           show_error:bool=False,
+                           hide:Optional[Union[Tuple[float, float], Callable]]=None):
+        x, weights = self.get_sample_data(samples, column_name,
+                                          variable_scale=variable_scale,
+                                          weight_scale=weight_scale,
+                                          weight_name=weight_name)
+        bin_edges = np.histogram_bin_edges(x,
+                                           bins=hist_options["bins"],
+                                           range=hist_options["bin_range"])
+        show_xerr = show_error and self.config['show_xerr']
+        hist_data = get_hist_data(x, weights, 
+                                  xerr=show_xerr,
+                                  yerr=show_error,
+                                  error_option='auto',
+                                  **hist_options)
+        handles = self.draw_binned_data(ax, hist_data,
+                                        bin_edges=bin_edges,
+                                        styles=styles,
+                                        draw_error=show_error,
+                                        plot_format=plot_format,
+                                        error_format=error_format,
+                                        error_styles=error_styles,
+                                        hide=hide)
+        self.hist_data[target] = hist_data
+        self.hist_bin_edges[target] = bin_edges
+        self.update_legend_handles({target: handles})
             
     def draw(self, column_name:str, weight_name:Optional[str]=None,
              targets:Optional[List[str]]=None, 
@@ -409,7 +527,7 @@ class VariableDistributionPlot(AbstractPlot):
              unit:Optional[str]=None, bins:Union[int, Sequence]=25,
              bin_range:Optional[Sequence]=None, clip_weight:bool=True,
              underflow:bool=False, overflow:bool=False, divide_bin_width:bool=False,
-             normalize:bool=True, show_error:bool=False, show_error_legend:bool=False,
+             normalize:bool=True, show_error:bool=False,
              stacked:bool=False, xmin:Optional[float]=None, xmax:Optional[float]=None,
              ymin:Optional[float]=None, ymax:Optional[float]=None, ypad:float=0.3,
              variable_scale:Optional[float]=None, logy:bool=False,
@@ -453,8 +571,10 @@ class VariableDistributionPlot(AbstractPlot):
                 content could be less than one.
             show_error: bool, default = False
                 Whether to display data error.
-            show_error_legend: bool, default = False
-                Whether to include legend for the error artists.
+            stacked: bool, default = False
+                Do a stacked plot. Only histograms will be stacked (i.e. plot format
+                is not errorbar). Samples with different stack_index will be stacked
+                independently.
             xmin: (optional) float
                 Minimum range of x-axis.
             xmax: (optional) float
@@ -476,103 +596,65 @@ class VariableDistributionPlot(AbstractPlot):
             legend_order: (optional) list of str
                 Order of legend labels. The same order as targets will be used by default.
         """
-        plot_options = self.resolve_plot_options(self.plot_options, targets=targets)
-        comparison_options = self.resolve_comparison_options(comparison_options,
-                                                             plot_options)
+        plot_options = self.resolve_plot_options(targets=targets)
+        relevant_samples = remove_duplicates([sample for options in plot_options.values() \
+                                             for sample in options['samples']])
+        if not relevant_samples:
+            raise RuntimeError('no targets to draw')
+        if stacked:
+            # this will remove targets that participates in stacking
+            stacked_plot_options = self.resolve_stacked_plot_options(plot_options)
+        else:
+            stacked_plot_options = {}
+        comparison_options = self.resolve_comparison_options(comparison_options, plot_options)
         if legend_order is not None:
             self.legend_order = list(legend_order)
         else:
             self.legend_order = list(plot_options)
-        if show_error_legend and (not stacked):
-            self.legend_order.extend([f"{target}_error" for target in self.legend_order])
+            for options in stacked_plot_options.values():
+                self.legend_order.extend([target for target in list(options['components']) \
+                                          if target not in self.legend_order])
                 
         if comparison_options is not None:
-            ax, ax_ratio = self.draw_frame(ratio_frames, logy=logy,
-                                           **self.styles["ratio_frames"])
+            ax, ax_ratio = self.draw_frame(ratio=True, logy=logy)
         else:
-            ax = self.draw_frame(logy=logy)
+            ax = self.draw_frame(ratio=False, logy=logy)
             
         if (bin_range is None) and isinstance(bins, (int, float)):
-            relevant_samples = [sample for options in plot_options.values() \
-                                for sample in options['samples']]
             bin_range = self.deduce_bin_range(relevant_samples, column_name, variable_scale=variable_scale)
             self.stdout.info(f"Using deduced bin range ({bin_range[0]:.3f}, {bin_range[1]:.3f})")
 
         self.reset_hist_data()
-        binned_data   = {}
-        target_bin_edges = {}
+        hist_options = {
+                        "bins" : bins,
+                   "bin_range" : bin_range,
+                   "underflow" : underflow,
+                    "overflow" : overflow,
+                   "normalize" : normalize,
+                 "clip_weight" : clip_weight,
+            "divide_bin_width" : divide_bin_width
+        }
+        data_options = {
+            'column_name': column_name,
+            'weight_name': weight_name,
+            'variable_scale': variable_scale
+        }
         
-        stacked_plot_options = {}
-        if stacked:
-            stack_targets = [target for target, options in plot_options.items() if \
-                             options['plot_format'] == PlotFormat.HIST]
-            if not stack_targets:
-                raise RuntimeError('no histograms to be stacked')
-            for target in stack_targets:
-                options = plot_options.pop(target)
-                stack_index = options['stack_index']
-                if stack_index not in stacked_plot_options:
-                    stacked_plot_options[stack_index] = {}
-                stacked_plot_options[stack_index][target] = options
-            for stack_index, stacked_plot_options_i in stacked_plot_options.items():
-                bin_edges, hist_data = self.draw_stacked(ax, stacked_plot_options_i,
-                                                         column_name=column_name,
-                                                         weight_name=weight_name,
-                                                         bins=bins, bin_range=bin_range,
-                                                         underflow=underflow,
-                                                         overflow=overflow,
-                                                         normalize=normalize,
-                                                         clip_weight=clip_weight,
-                                                         divide_bin_width=divide_bin_width,
-                                                         variable_scale=variable_scale)
-                label = self.config['stacked_label'].format(index=stack_index)
-                binned_data[label] = hist_data
-                target_bin_edges[label] = bin_edges
+        for stack_target, options in stacked_plot_options.items():
+            options['show_error'] = options.get('show_error', show_error)
+            self.draw_stacked_target(ax, stack_target=stack_target,
+                                     hist_options=hist_options,
+                                     **options,
+                                     **data_options)
+            
         for target, options in plot_options.items():
-            samples, styles, error_styles = options['samples'], options['styles'], options['error_styles']
-            label = styles['label']
-            weight_scale = options['weight_scale']
-            show_this_error = options.get('show_error', show_error)
-            plot_format, error_format = options['plot_format'], options['error_format']
-            x, weights = self.get_sample_data(samples, column_name,
-                                              variable_scale=variable_scale,
-                                              weight_scale=weight_scale,
-                                              weight_name=weight_name)
-            bin_edges = np.histogram_bin_edges(x, bins=bins, range=bin_range)
-            hist_data = get_hist_data(x, weights, underflow=underflow,
-                                      overflow=overflow, normalize=normalize,
-                                      divide_bin_width=divide_bin_width,
-                                      bin_range=bin_range, bins=bins,
-                                      clip_weight=clip_weight,
-                                      xerr=show_this_error and self.config['show_xerr'],
-                                      yerr=show_this_error,
-                                      error_option='auto')
-            binned_data[target] = hist_data
-            target_bin_edges[target] = bin_edges
-            if plot_format == PlotFormat.HIST:
-                # draw data
-                hist_y, _, handle = ax.hist(hist_data['x'], bins, range=bin_range,
-                                            weights=hist_data['y'], **styles)
-                assert np.allclose(hist_data['y'], hist_y)
-                # draw error only
-                handles = self.draw_binned_data(ax, hist_data,
-                                                bin_edges=bin_edges,
-                                                draw_data=False,
-                                                draw_error=show_this_error,
-                                                error_format=error_format,
-                                                error_styles=error_styles)
-                if not isinstance(handle, list):
-                    handle = [handle]
-                handles = tuple(list(handles) + handle)
-            elif plot_format == PlotFormat.ERRORBAR:
-                handles = self.draw_binned_data(ax, hist_data,
-                                                bin_edges=bin_edges,
-                                                styles=styles,
-                                                draw_error=show_this_error,
-                                                error_format=error_format,
-                                                error_styles=error_styles)
+            options['show_error'] = options.get('show_error', show_error)
+            options.pop('stack_index', None)
+            self.draw_single_target(ax, target=target,
+                                    hist_options=hist_options,
+                                    **options,
+                                    **data_options)
             
-            self.update_legend_handles({target:handles})
         # propagate bin width to ylabel if needed
         if isinstance(bins, int):
             bin_width = (bin_range[1] - bin_range[0]) / bins
@@ -590,11 +672,10 @@ class VariableDistributionPlot(AbstractPlot):
                 
         if not self.is_single_data():
             handles, labels = self.get_legend_handles_labels()
-            box_legend_handle = self.config['box_legend_handle']
-            if not box_legend_handle:
+            if not self.config['box_legend_handle']:
                 handles = remake_handles(handles, polygon_to_line=True,
                                          line2d_styles=self.styles['legend_Line2D'])
-            ax.legend(handles=handles, labels=labels, **self.styles['legend'])
+            self.draw_legend(ax, handles=handles, labels=labels)
         
         if comparison_options is not None:
             components = comparison_options.pop('components')
@@ -611,9 +692,6 @@ class VariableDistributionPlot(AbstractPlot):
             self.decorate_comparison_axis(ax_ratio, **comparison_options)
             ax.set(xlabel=None)
             ax.tick_params(axis="x", labelbottom=False)
-
-        if self.config['save_hist_data']:
-            self.hist_data = binned_data
             
         if comparison_options is not None:
             return ax, ax_ratio
diff --git a/quickstats/utils/common_utils.py b/quickstats/utils/common_utils.py
index ad88fd8c15db5278396254c95bf575dacc4024f5..734ee46ca6b63518575fefe7537bd8fab0cc9267 100644
--- a/quickstats/utils/common_utils.py
+++ b/quickstats/utils/common_utils.py
@@ -1,4 +1,4 @@
-from typing import Optional, Union, Dict, List, Tuple, Callable
+from typing import Optional, Union, Dict, List, Tuple, Callable, Any
 import os
 import sys
 import copy
@@ -13,8 +13,6 @@ import collections.abc
 
 import numpy as np
 
-from .string_utils import split_str, parse_as_dict
-
 class disable_cout:    
     def __enter__(self):
         import cppyy
@@ -427,6 +425,7 @@ def filter_dataframe_by_index_values(df, index_values:Union[Tuple[List], List],
     return df
 
 def parse_config_dict(expr:Optional[Union[str, Dict]]=None):
+    from .string_utils import parse_as_dict
     if expr is None:
         return {}
     if isinstance(expr, str):
@@ -453,10 +452,29 @@ def list_of_dict_to_dict_of_list(source:List[Dict], use_first_keys:bool=True):
 def dict_of_list_to_list_of_dict(source:Dict[str, List]):
     return [dict(zip(source, t)) for t in zip(*source.values())]
 
-def save_as_json(data:Dict, outname:str,
+def save_json(data: Dict, outname: str, indent: int = 2, truncate: bool = False) -> None:
+    """
+    Serializes a dictionary to a JSON file.
+
+    Parameters:
+    data (Dict): The dictionary object to serialize to JSON.
+    outname (str): The file path where the JSON output will be saved.
+    indent (int): The number of spaces to use for indentation in the JSON file. Default is 2.
+    truncate (bool): If True, the file will be truncated at the end of the JSON data. Default is False.
+                     Typically not needed unless dealing with file updates where the new data might
+                     be shorter than the old data.
+    """
+    with open(outname, "w") as file:
+        json.dump(data, file, indent=indent)
+        # truncate the file if the flag is True; this might be useful in case the new JSON data is shorter
+        # than any existing data in the file to prevent old data from remaining at the end of the file.
+        if truncate:
+            file.truncate()
+
+def save_json(data:Dict, outname:str,
                  indent:int=2, truncate:bool=True):
     with open(outname, "w") as file:
-        json.dump(data, file, indent=2)
+        json.dump(data, file, indent=indent)
         if truncate:
             file.truncate()
         
@@ -476,14 +494,39 @@ def filter_dataframe_by_column_values(df:"pd.DataFrame", attributes:Dict):
         else:
             df = df[df[attribute] == value]
     df = df.reset_index(drop=True)
-    return df      
+    return df
 
 class IndentDumper(yaml.Dumper):
-    def increase_indent(self, flow=False, indentless=False):
-        return super(IndentDumper, self).increase_indent(flow, False)
+    """
+    A custom YAML Dumper that allows for increased indentation control.
+    """
 
-def save_yaml(obj, filename:str, indent:int=2):
+    def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
+        super().increase_indent(flow, False)
+
+def save_yaml(obj: Any, filename: str, indent: int = 2) -> None:
+    """
+    Saves a Python object to a YAML file with custom indentation.
+
+    Parameters:
+    obj (Any): The Python object to serialize and save to YAML.
+    filename (str): The path to the file where the YAML output should be written.
+    indent (int): The number of spaces to use for indentation. Default is 2.
+    """
     with open(filename, 'w') as f:
         yaml.dump(obj, f, Dumper=IndentDumper,
                   default_flow_style=False,
-                  sort_keys=False, indent=indent)
\ No newline at end of file
+                  sort_keys=False, indent=indent)
+
+def remove_duplicates(lst):
+    """
+    Removes duplicates from a list while preserving the original order of elements.
+
+    Parameters:
+    lst (list): The list from which duplicates are to be removed.
+
+    Returns:
+    list: A new list containing the unique elements of the original list in the order they first appeared.
+    """
+    seen = set()
+    return [x for x in lst if not (x in seen or seen.add(x))]
\ No newline at end of file
diff --git a/quickstats/utils/data_conversion.py b/quickstats/utils/data_conversion.py
index 257ae50aa4f9cb4d9fb30c0eab753cb98fd5eaf3..e2a62cc330aaac4e8651356cd35de3bd8cfd03f4 100644
--- a/quickstats/utils/data_conversion.py
+++ b/quickstats/utils/data_conversion.py
@@ -16,7 +16,7 @@ root_datatypes = ["bool", "Bool_t", "Byte_t", "char", "char*", "Char_t",
                   "UInt_t", "ULong64_t", "ULong_t",
                   "unsigned", "unsigned char", "unsigned int",
                   "unsigned long", "unsigned long long",
-                  "unsigned short", "UShort_t"]
+                  "unsigned short", "UShort_t", "ROOT::VecOps::RVec<Char_t>"]
 
 uproot_datatypes = ["bool", "double", "float", "int", "int8_t", "int64_t", "char*", "int32_t", "uint64_t", "uint32_t"]
 
diff --git a/quickstats/utils/path_utils.py b/quickstats/utils/path_utils.py
index 2a3e5f20ff87d9c061de87fdcbacb6b42f8a2634..ff23b69b36c2e4abaf2259dc30aa0150eea35a87 100644
--- a/quickstats/utils/path_utils.py
+++ b/quickstats/utils/path_utils.py
@@ -28,40 +28,23 @@ def is_remote_path(path:str):
 def is_xrootd_path(path:str):
     return "root://" in path
 
-def remote_glob(path:str):
-    # can only glob xrootd path
-    if not is_xrootd_path(path):
-        return path
-    import XRootD.client.glob_funcs as glob
-    return glob.glob(path)
+def remote_file_exist(path:str, timeout:int=0):
+    from quickstats.interface.xrootd.path import exists
+    return exists(path, timeout=timeout)
 
-def get_filesystem(host:str):
-    if host in FILESYSTEM_TO:
-        return FILESYSTEM_TO[host]
-    from XRootD.client import FileSystem
-    FILESYSTEM_TO[host] = FileSystem(host)
-    return get_filesystem(host)
+def remote_glob(path:str):
+    from quickstats.interface.xrootd.path import glob as remote_glob
+    return remote_glob(path)
 
-def remote_isdir(dirname:str, timeout:int=0):
-    # can only list xrootd dir
-    if not is_xrootd_path(dirname):
-        return None
-    from XRootD.client import FileSystem
-    host, path = split_url(dirname)
-    query = get_filesystem(host)
-    if not query:
-        raise RuntimeError("Cannot prepare xrootd query")
-    status, dirlist = query.dirlist(path, timeout=timeout)
-    return not status.error
-    #return len(remote_glob(os.path.join(dirname, "*"))) > 0
+def remote_isdir(dirname:str, timeout:int=0): 
+    from quickstats.interface.xrootd.path import isdir
+    return isdir(dirname, timeout=timeout)
 
-def remote_dirlist(dirname:str):
-    # can only list xrootd dir
-    if not is_xrootd_path(dirname):
-        return []
+def remote_listdir(dirname:str):
+    from quickstats.interface.xrootd.path import glob as remote_glob
     return remote_glob(os.path.join(dirname, "*"))
 
-def dirlist(dirname:str):
+def listdir(dirname:str):
     return glob.glob(os.path.join(dirname, "*"))
 
 def local_file_exist(path:str):
@@ -71,18 +54,6 @@ def local_file_exist(path:str):
         host, path = split_url(path)
         return local_file_exist(path)
     return False
-
-def remote_file_exist(path:str, timeout:int=0):
-    # can not stat non-xrootd file for now
-    if not is_xrootd_path(path):
-        return None
-    from XRootD.client import FileSystem
-    host, path = split_url(path)
-    query = get_filesystem(host)
-    if not query:
-        raise RuntimeError("Cannot prepare xrootd query")
-    status, _ = query.stat(path, timeout=timeout)
-    return not status.error
     
 def resolve_paths(paths:Union[str, List[str]],
                   sep:str=","):
@@ -93,11 +64,11 @@ def resolve_paths(paths:Union[str, List[str]],
     for path in paths:
         if "*" in path:
             if is_remote_path(path):
-                glob_paths = remote_glob(path)
+                from quickstats.interface.xrootd.path import glob
+                glob_paths = glob(path)
             else:
                 glob_paths = glob.glob(path)
             resolved_paths.extend(glob_paths)
         else:
             resolved_paths.append(path)
-    return resolved_paths
-
+    return resolved_paths
\ No newline at end of file
diff --git a/quickstats/utils/roofit_utils.py b/quickstats/utils/roofit_utils.py
index f03424c7a07865bc092118714093549dbf59be70..8c41773ac55a3844ea45e1497e6b248695462f78 100644
--- a/quickstats/utils/roofit_utils.py
+++ b/quickstats/utils/roofit_utils.py
@@ -7,6 +7,7 @@ import numpy as np
 
 import ROOT
 
+from quickstats import root_version
 from .string_utils import remove_whitespace, split_str
 
 def copy_attributes(source:"ROOT.RooAbsArg", target:"ROOT.RooAbsArg"):
@@ -384,17 +385,26 @@ def get_gaus_response_variations(nuis:ROOT.RooRealVar, client:ROOT.RooAddition):
     value = round(magnitude * beta, 8)
     return {"nominal": nominal, "low": value, "high": value, "type": "gaus"}
 
-def get_logn_response_variations(nuis:ROOT.RooRealVar, client:ROOT.RooFormulaVar):
+def _get_formula_str(formula_var:"ROOT.RooFormulaVar"):
+    if root_version > (6, 26, 0):
+        return formula_var.expression()
+    return formula_var.formula().formulaString()
+
+def _get_formula_dependents(formula_var:"ROOT.RooFormulaVar"):
+    if root_version > (6, 26, 0):
+        return formula_var.dependents()
+    return formula_var.formula().actualDependents()
+
+def get_logn_response_variations(nuis:"ROOT.RooRealVar", client:"ROOT.RooFormulaVar"):
     result = {"nominal": None, "low": None, "high": None, "type": None}
     if not isinstance(client, ROOT.RooFormulaVar):
         raise ValueError("lognormal response function must be an instance of RooFormulaVar")    
     nuis_name = nuis.GetName()
-    formula = client.formula()
-    formula_str = formula.formulaString()
+    formula_str = _get_formula_str(client)
     formula_str = remove_whitespace(formula_str)
     if not formula_str.startswith("exp("):
         return result
-    dependents = formula.actualDependents()
+    dependents = _get_formula_dependents(client)
     if dependents.size() != 2:
         return result
     beta_term, nuis_term, resp_term = None, None, None
@@ -406,10 +416,10 @@ def get_logn_response_variations(nuis:ROOT.RooRealVar, client:ROOT.RooFormulaVar
     if any(term is None for term in [beta_term, nuis_term, resp_term]):
         return result
     beta = beta_term.getVal()
-    resp_formula_str = resp_term.formula().formulaString()
+    resp_formula_str = _get_formula_str(resp_term)
     resp_formula_str = remove_whitespace(resp_formula_str)
     if resp_formula_str == "log(1+x[0]/x[1])":
-        resp_dependents = resp_term.formula().actualDependents()
+        resp_dependents = _get_formula_dependents(resp_term)
         magnitude = resp_dependents[0].getVal()
         value = round(magnitude * beta, 8)
         nominal = resp_dependents[1].getVal()
diff --git a/quickstats/utils/root_utils.py b/quickstats/utils/root_utils.py
index 058842c9152aeae23e1adbe3376d4116052c98a3..0bd0c4cd389fde80a65be16ba2a4c6927510f2f7 100644
--- a/quickstats/utils/root_utils.py
+++ b/quickstats/utils/root_utils.py
@@ -7,6 +7,7 @@ import numpy as np
 
 import ROOT
 import quickstats
+from .common_utils import get_cpu_count
 
 root_type_str_maps = {
     'Char_t'    : 'char',
@@ -39,7 +40,7 @@ def templated_rdf_snapshot(rdf:ROOT.RDataFrame, columns:List[str]=None):
 
 def is_corrupt(f:Union[ROOT.TFile, str]):
     if isinstance(f, str):
-        f = ROOT.TFile(f)
+        f = ROOT.TFile.Open(f)
     if f.IsZombie():
         return True
     if f.TestBit(ROOT.TFile.kRecovered):
@@ -378,4 +379,45 @@ def get_cachedir():
     cachedir = ROOT.TFile.GetCacheFileDir()
     if (not cachedir) or (cachedir == "/"):
         return None
-    return cachedir
\ No newline at end of file
+    return cachedir
+
+def set_multithread(num_threads:Union[int, bool]=None):
+    if num_threads:
+        if num_threads > 1:
+            ROOT.EnableImplicitMT(num_threads)
+        else:
+            ROOT.EnableImplicitMT()
+            num_threads = get_cpu_count()
+    else:
+        num_threads = None
+        if ROOT.IsImplicitMTEnabled():
+            ROOT.DisableImplicitMT()
+    return num_threads
+
+def get_tree_perf_stats(tree):
+    # https://eguiraud.web.cern.ch/eguiraud/decks/root_io_perf_tooling/#/6
+    ps = ROOT.TTreePerfStats("ioperf", tree)
+    for i in range(tree.GetEntriesFast()):
+        tree.GetEntry(i)
+    ps.Print() # or ps.GetXXX(), or ps.Draw()
+
+def print_tree_clusters(tree):
+    tree.Print("clusters")
+
+def set_task_per_worker_hint(m:int):
+    ROOT.TTreeProcessorMT.SetTasksPerWorkerHint(m)
+
+def get_task_per_worker_hint():
+    return ROOT.TTreeProcessorMT.GetTasksPerWorkerHint()
+
+def get_opt_flag():
+    return ROOT.gSystem.GetFlagsOpt()
+
+def get_misc_summary():
+    summary = {
+        'opt_flag': get_opt_flag(),
+        'multithread': ROOT.IsImplicitMTEnabled(),
+        'task_per_worker_hint': get_task_per_worker_hint(),
+        'cachedir': get_cachedir()
+    }
+    return summary
\ No newline at end of file
diff --git a/quickstats/utils/string_utils.py b/quickstats/utils/string_utils.py
index e2071f0d4eb26f21912d4cf5c8123b8caba29d26..976445047fbfbff364f1c8773b97336a35570912 100644
--- a/quickstats/utils/string_utils.py
+++ b/quickstats/utils/string_utils.py
@@ -90,12 +90,7 @@ def split_str(s: str, sep: str = None, strip: bool = True, remove_empty: bool =
     items = [cast(item) if item else empty_value for item in items]
 
     return items
-
-def split_str_excl_paranthesis(s: str, sep: str = ",", strip: bool = True, remove_empty: bool = False) -> List:
-    regex = re.compile(sep + r'\s*(?![^()]*\))')
     
-
-
 whitespace_trans = str.maketrans('', '', " \t\r\n\v")
 newline_trans = str.maketrans('', '', "\r\n")
 
@@ -347,4 +342,69 @@ def format_dict_to_string(dictionary: Dict[str, str], separator: str = " : ",
         line = f"{' ' * left_margin}{key:{max_key_length}}{separator}{wrapped_value}"
         formatted_lines.append(line)
 
-    return "\n".join(formatted_lines) + "\n"
\ No newline at end of file
+    return "\n".join(formatted_lines) + "\n"
+
+
+def str_to_bool(s:str) -> bool:
+    """
+    Convert a string into a boolean value.
+    
+    Parameters:
+    s (str): The string to convert.
+
+    Returns:
+    bool: The boolean value of the string.
+
+    Raises:
+    ValueError: If the string does not represent a boolean value.
+    """
+    s = s.strip().lower()
+
+    true_values = {'true', '1'}
+    false_values = {'false', '0'}
+
+    if s in true_values:
+        return True
+    elif s in false_values:
+        return False
+    else:
+        raise ValueError(f"Invalid literal for boolean: '{s}'")
+
+def remove_cpp_type_casts(expression: str) -> str:
+    """
+    Removes type casts from a C++ expression based on general structure.
+
+    Parameters:
+    expression (str): A string containing a C++ expression.
+
+    Returns:
+    str: The expression with type casts removed.
+    """
+    # Matches a parenthetical that seems like a type (any word potentially followed by pointer/reference symbols),
+    # ensuring it's not preceded by an identifier character and is followed by a valid variable name.
+    type_cast_pattern = r'(?<![\w_])\(\s*[a-zA-Z_]\w*\s*[\*&]*\s*\)\s*(?=[a-zA-Z_]\w*|[+-]?\s*\d|\.)'
+    return re.sub(type_cast_pattern, '', expression)
+
+def extract_variable_names(expression:str)->List[str]:
+    """
+    Extracts variable names from a C++ expression.
+
+    Parameters:
+    expression (str): A string containing a C++ expression.
+
+    Returns:
+    list: A list of unique variable names found in the expression.
+    """
+
+    expression = remove_cpp_type_casts(expression)
+
+    # Match potential variable names which are not directly followed by a '(' which would indicate a 
+    # function call. Use negative lookaheads and positive lookbehinds to refine the match.
+    pattern = r'\b[a-zA-Z_]\w*(?:\.\w+)*\b(?!\s*\()'
+
+    matches = re.findall(pattern, expression)
+
+    from quickstats.utils.common_utils import remove_duplicates
+    unique_matches = remove_duplicates(matches)
+    
+    return unique_matches
\ No newline at end of file
diff --git a/quickstats/utils/sys_utils.py b/quickstats/utils/sys_utils.py
index 871da7add531f19ca8ed465d75b514a2d418ef9a..9da786432bd15edd6df291d79fa6c936fb194e79 100644
--- a/quickstats/utils/sys_utils.py
+++ b/quickstats/utils/sys_utils.py
@@ -41,4 +41,34 @@ def set_argv(cmd: str, expandvars:bool=True):
     # Use shlex.split to correctly parse the command line string into arguments,
     # handling cases with quotes and escaped characters appropriately.
     parsed_args = shlex.split(cmd)
-    sys.argv = parsed_args
\ No newline at end of file
+    sys.argv = parsed_args
+
+def bytes_to_readable(size_in_bytes, digits=2):
+    """
+    Convert the number of bytes to a human-readable string format.
+
+    Parameters:
+    size_in_bytes (int): The size in bytes that you want to convert.
+    digits (int, optional): The number of decimal places to format the output. Default is 2.
+
+    Returns:
+    str: A string representing the human-readable format of the size.
+
+    Examples:
+    >>> bytes_to_readable(123456789)
+    '117.74 MB'
+
+    >>> bytes_to_readable(9876543210)
+    '9.20 GB'
+
+    >>> bytes_to_readable(123456789, digits=4)
+    '117.7383 MB'
+
+    >>> bytes_to_readable(999, digits=1)
+    '999.0 B'
+    """
+    for unit in ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']:
+        if abs(size_in_bytes) < 1024.0:
+            return f"{size_in_bytes:.{digits}f} {unit}"
+        size_in_bytes /= 1024.0
+    return f"{size_in_bytes:.{digits}f} YB"
\ No newline at end of file
diff --git a/tutorials/VariableDistributionPlot.ipynb b/tutorials/VariableDistributionPlot.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..79846f8723dab1812534ad0586a82a9f40bf3e7f
--- /dev/null
+++ b/tutorials/VariableDistributionPlot.ipynb
@@ -0,0 +1,188 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "d8299c75-7b71-428d-bfbb-708dd1e6889a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "\n",
+    "# Define the number of data points\n",
+    "n = 10000\n",
+    "\n",
+    "# Generate random data for each distribution\n",
+    "data_uniform = np.random.uniform(low=0, high=100, size=n)\n",
+    "data_gaussian = np.random.normal(loc=50, scale=10, size=n)\n",
+    "\n",
+    "data_poisson = np.random.poisson(lam=50, size=n)\n",
+    "\n",
+    "# Create DataFrames\n",
+    "df_uniform = pd.DataFrame(data_uniform, columns=['x'])\n",
+    "df_gaussian = pd.DataFrame(data_gaussian, columns=['x'])\n",
+    "df_poisson = pd.DataFrame(data_poisson, columns=['x'])\n",
+    "\n",
+    "# Create a dictionary of the DataFrames\n",
+    "dfs = {\n",
+    "    'uniform': df_uniform,\n",
+    "    'gaussian': df_gaussian,\n",
+    "    'poisson': df_poisson\n",
+    "}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "cd99a2e4-4b56-4db4-82cc-6789c70db955",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "from quickstats.plots import VariableDistributionPlot\n",
+    "plot_options = {\n",
+    "    'uniform': {\n",
+    "        # hide with custom function\n",
+    "        'hide': lambda x: (x < 10) | ((x > 40) & ( x < 50)) | (x > 80),\n",
+    "        'plot_format': 'hist',\n",
+    "        'error_format': 'fill'\n",
+    "    },\n",
+    "    'gaussian': {\n",
+    "        'error_format': 'errorbar'\n",
+    "    },\n",
+    "    'poisson': {\n",
+    "        # hide range\n",
+    "        'hide': (50, 60),\n",
+    "        'error_format': 'shade'\n",
+    "    }\n",
+    "}\n",
+    "plotter = VariableDistributionPlot(dfs, plot_options=plot_options)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "0a6822c1-80fa-4cb4-8c92-fdbcb3816ca6",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[INFO] Using deduced bin range (0.008, 99.992)\n",
+      "Welcome to JupyROOT 6.30/04\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<Axes: ylabel='Fraction of Events / 4.00'>"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<Figure size 640x480 with 0 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "image/png": "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5HK5PPu99dZbMgxDa9as8Rwn6z6uQYMGatiwYbZyFi5cqD59+qhq1apq0KCBBg8erC1btmTbJuuY69ev13//+1/Vrl1bQ4YMyRbbgQMH9NRTT+mKK65Q1apVNXDgQO3YsUPJycmaPHmy2rRpo7CwMHXq1EnfffedJZ9ffjgcjjzvb7255LBQr732mm6++Wa9/PLL+uqrr7Rq1ao8t23SpIluv/12TZo0SZGRkb6fRSlGJ28AAAD/9frrr+vf//63evfurfbt2+uzzz7TP/7xD9lsNj366KO57nPXXXdpxYoV+vHHH3XXXXepQYMGnnWTJ0/WzJkzVaNGDfXp00dJSUlauXKlvvjiC33wwQeeJCLLvHnz9Oabb+raa6/Vtddem23dX//6V23evFnXXnutTNPUZ599pl9++UVNmzbVtm3b1KdPH1WsWFE//PCDbrrpJv3+++/Fcq/tcDjyfFDuLcnI10ze/fr1U79+/eR0OrV7924dPnxYSUlJysjIUI0aNVSzZk3VqlVLVatWLVDwAAAAKD5nbuta0iHky5k1KxT54TpLjvWf//xH69evV4cOHSRJe/bsUdu2bbV48eI8E4zp06fLZrPpxx9/1BNPPKH69etLkn7++We98MILuuKKK7RixQpVq1ZNkrRlyxb17t1bDz30kG644YZsrXneeecdff/99zn6eEjufh7bt29XZGSkXC6XoqOj9dtvv8lms2n79u2qXr26JGnQoEFatmyZfvzxR11//fWWfC5FwafhoQICAtSqVStdd911uuWWWzRs2DD17t1brVq1IrkAAACA35owYYInuZCkZs2aqV27djp58qTPx3r99dflcrn0/PPPe5ILSWrfvr0eeOABHTx4UMuXL8+2z/Dhw3NNLiR3bUhWjYTNZtOAAQMkSY888ognuZCk/v37S5JOnz7tc8zFybfxZwEAAIBS6Oqrr86xLDQ0tEDH2rNnj4KCgtStW7cc63r37i1J+u2337Itb9WqVZ7Ha9asWbb3WUlL1tQPhY23uOWriZSvxowZo7Vr1+b4YAEAAFDyIj9cp3OvPOX382CE3f9YgfbNrW/sxTUBhXXkyBHVqFEj17niatd2f6aHDh3KtrxWrVp5Hi8gIMCn5f6uSGowjh8/rv379xfFoQEAAACvcmtCdKmRj3xRp04dnThxItsIVFmOH3fPjp6VaGTxdeLq0qxIzvSRRx7R/Pnzi+LQAAAAgEdqamq297t27dK5c+eKtMymTZsqLS1N69evz7Hum2++kZSz2VN5UiQJxrXXXqu77rqrKA4NAAAAeAYYWrFihWdZWlpaniNCWWnChAkyDEMPP/ywzpw541keExOj//znP6pbt65uuOGGIo/DXxVJHwwAAACgKA0ZMkSzZs3SsGHDNHz4cFWpUkVffvmlXC6XOnfuXKRlt2/fXhMnTtTs2bPVsmVL9ezZU0lJSfrmm2/kcrk0f/58hYSEFGkM/sySBGPKlCnavHmzvvrqKysOBwAAgCJmq1ZLrpPHSjqMPNmq5d0pWpKuueYaffTRR/rXv/6lhQsXqkGDBurZs6emT5+uESNG6MKFC0Ua36xZs9SxY0fNnTtXq1atUlhYmK677jo5HA5dfvnlRVq2vzNMC6agvuGGG/Tll1/K6XRaEVMOTqdTzz77rBYuXKi9e/eqTp06uueee/TII4/ka/8ff/xR06ZN0/bt25WUlKRmzZppzJgxuueee3L0zk9JSdHjjz+uzz77TAcOHFDjxo01efLkPJt8ZXUYYiZvAAAAlBfe7oG91mDMmTMnXwUcPHhQkjR37txshdx99935DtKbCRMmaN68eWrQoIEGDx6sDRs2aPLkyUpNTdXUqVO97rtixQoNGDBAFSpUULdu3VS9enV99913uv/++7Vp06ZsndFN09SgQYP01VdfqWXLlrrpppu0evVqjRo1SjabTSNGjLDkfAAAAICyymsNhs1myzakl2ma+RriK2s7K2o01q1bp27duqlHjx5atWqV7Ha70tPT1adPH3333Xf66aefss3K+GctW7bUwYMHtWHDBrVp00aSdOHCBfXr109r165VTEyM2rdvL0l69913NXLkSA0fPlzvvfeeJCkhIUGdO3fWvn37dPDgQdWsWTPb8anBAAAAQHlT4BqMd955Rw888IASEhLUvHlz3X777bkmGO+++65+//13TZs2zaKQ/5BVwzBr1izZ7XZJkt1u1+zZs9WhQwctXrw4zwTj1KlT2rVrl+68805PciFJFSpU0AMPPKC1a9dq8+bNngRj/vz5stlsevHFFz3bhoeHa8aMGbr55pv16aefasKECZafIwAAAFBWeE0w7rzzTnXr1k0jRozQunXrtH37dv3vf/9TZGRktu02btyo33//XY8//rjlAa5fv15RUVGeJCBL+/btFRUVpaVLl2rGjBm57nv69Gl17dpVPXr0yLEuODhY0h+TnrhcLm3cuFGdO3f2TM+epW/fvgoKCtLSpUtJMAAAAAAvLjkPRr169bRmzRo9+eSTWrZsmdq2bauVK1cWR2zKyMjQ3r171bx581zXR0dHKzY2Ns/9o6OjtXbt2hx9QUzT1DvvvCNJ6tWrlyQpNjZWycnJuZYVFhamOnXqeC0LAAAAQD4n2jMMQ1OnTtX69esVGhqq/v3766GHHsoxc6LVEhMT5XK5FBERkev6yMhInT9/XikpKfk63r///W/dcccdql+/vj777DPNnz9fjRo1kiSdPXtWkryWderUqTyPbRhGgV8AAABAcSuq+1efZvLu2LGjtm7dqrFjx+o///mPrrjiCm3durUw5+VVVuIQFhaW6/qs5fHx8fk63tKlS/XBBx/o0KFDCg8Pz/bh5Kesi2dqBAAAAJCTTwmGJIWGhmrOnDn6+OOPdeLECXXu3FmbN28uitg8fT2SkpJyXZ+QkCBJqlKlSr6Ot3btWp07d07r169XgwYNNHr0aC1YsCDfZeVVuyG5m10V9AUAAAAUt6K6f/U5wcgyePBgbdu2TT169PDadKgwQkJCVKlSpTxrDuLj41W5cmWFhobm+5gVK1ZUly5d9PHHH8swDM9cH1nDz3orq1Yt7zNKAgAAAOVdgRMMSapVq5a+/PJLnT17Nt/NlHwVFRWlXbt25ciUTNPU7t27FRUVlee+n3zyie6++27t3Lkzx7o6deqoZs2aiouLk+SuwahQoUKu2yYmJurIkSNeywIAAABQyAQjS+XKlVW5cmUrDpXDwIEDFRcXp5iYmGzLY2JiFBcXpwEDBuS5b3JysubOnavvv/8+x7rz58/r5MmTatq0qWfZgAEDtHnzZp04cSLbtqtWrVJGRobXsgAAAMqrhuPnquH4uSUdBvyEJQlGURo9erQkadKkSUpPT5ckpaena9KkSZKksWPH5rlv9+7dFRwcrFmzZmWrYXE6nXr00UfldDp1/fXXZyvL5XJp4sSJnmUJCQmaOnWqKlSooOHDh1t5agAAAECZ43WiPX/QqlUrTZkyRc8884yio6PVuXNnbdiwQbGxsZo6dapatGjh2fbBBx+UJE2fPl2RkZGqW7euHn/8cU2dOlXNmjVTr169VKFCBf3www/as2ePrr32Wt1zzz2e/fv376/hw4drwYIF2r59u9q0aaPVq1crLi5Or732mtdO3gAAAAAkwywlwxi99NJLWrRokbZt26Z27drp9ttv13333Zdtm6xhZ2NjY1W/fn3P8iVLlmjWrFn69ddflZaWphYtWmj48OH6y1/+ooCAgGzHcDqdeuqpp/T5559r7969uvLKKzV+/HjddtttucaVVWYp+RgBAAAsl9U8av/c8SUcSem1Zs0a9ezZU2+++aZGjRpV0uFckrd74FKTYPgrEgwAAFDekWAU3m+//aYXX3xRI0aMUKdOnUo6nEvydg/stYnUTz/9pCuuuKJoogIAAAAgSWrSpIleeumlkg7DEl47effs2VMNGzbUI488oo0bNxZXTAAAAABKKa8JxsmTJ/XCCy/o+PHjuv7661W3bl1NnDhR3333HU2CAAAAUKJOnjypcePGqUmTJqpbt67uuusuxcXFqVmzZtmmIti+fbvuuOMO1a5dW8HBwapTp44GDhyYYyqDt956S4ZhaM2aNTnKGjNmjKdZUJadO3fqjjvuUP369RUaGqrmzZtr+vTpSklJ8Xm7NWvWyDAMvfXWW9n2Xbp0qXr37q2qVasqJCREjRo10oQJE3Tw4MEc8dntdpmmqZkzZ6pRo0YKDQ1V69atNXdu8Q4h7LWJVIUKFTRkyBANGTJEaWlpWrlypT7++GMNGTJEdrtdQ4YM0S233KJevXrJZvP7EW8BAABQRhw8eFDdu3f3/FunTh1988036tmzp4KCgpSamirJPfhPr169lJCQoD59+uiyyy7T4cOH9eWXX+rrr79WTEyMmjdv7nP5+/btU9euXZWQkKAOHTqoS5cuWrt2rRwOhw4fPqw5c+b4tF1uPv30U918882qVKmSrrvuOlWqVEm//PKL5syZo2+//Va7du1SYGD22/mpU6fqlVde0bXXXiubzabPPvtMd999typXrpznoEVWy/cwtUFBQRowYIAGDBggp9Opr7/+Wh999JGGDx8up9OpwYMHa+jQoerTp4/sdntRxgwAAIBCKKpJ8YriuHl1HJ82bZoOHDig999/X8OGDZMkJSYmqn///vr+++/VpEkTSdJnn32m06dPa+HChdlusBcuXKhhw4ZpxYoVBUow3nvvPcXHx2c7rtPpVIcOHfTuu+/qtddeU0BAQL63y80777yj4OBg/fzzz9lGSP3LX/6i1157Tb/88osuv/xyz/KMjAy999572rJlixo1aiTpj9GpFi9eXGwJRoGqHQICAtSvXz+9/vrrOnbsmBYtWqSQkBCNHz9eNWrU0F133aWlS5daHSsAAACg+Ph4vfPOO7r22ms9yYUkVa5cWc8880y2bbt27arFixdryJAh2ZZnJRUJCQkFiuHMmTOSlO3BekBAgJYtW6aNGzd6uhPkd7vc3HPPPVq6dGm25OJSsf/973/3JBeS1KNHD9WoUUMnT5709RQLrNAT7dlsNvXq1Uu9evXSSy+9pPXr12vx4sWaOHGiBg0aZEWMAAAAgMeePXvkdDrVu3fvHOu6dOmSrUbg8ssv1+WXXy7TNHXgwAHt3btXv/76q95+++1CxXDbbbfppZde0siRIzV8+HD16dNH11xzjerVq6d69er5vF1u+vbtK0lKT0/X77//rr1792rHjh1eR5u6+uqrcywLDQ0t4FkWjOUzeV9zzTW65pprNGvWLKsPDQAAAAtYPV9Fcc+DceDAAUlSzZo1c6wLCAhQ9erVPe+Tk5P1j3/8Q4sXL9bRo0cVERGhNm3a6IorrtCPP/6Y7zL/XNNw9dVXa+PGjZoxY4bee+89T1+Kjh076vHHH9fAgQN92i43+/fv1+TJk7Vy5UolJSWpTp06at++va666ip98sknue5z8bmXFHpmW8QwjDxfDoejpMMDAAAoM2rXri1JiouLy7HO5XLp9OnTnve33367XnnlFY0bN0779u3TmTNntGbNGv3973/3qcyLj5mlY8eO+vjjjz3HnDZtmo4cOaJBgwbp22+/9Xm7i6WkpKhz585au3atZs6cqTNnzujw4cNatmyZBg8enGecfx7pqjAcDkee97fekGBYxDTNPF8kGAAAANaJjo6WJK1evTrHuo0bNyo9PV2SdOHCBa1cuVIDBgzQE088oYYNG3q2O378eJ7Hv3DhQrb3GRkZ2rJlS7Zlr732mqZPny5JCg4OVvfu3eVwOPTmm29Kcncu92W73M7jxIkT+sc//qEJEyYoIiIiX7FbyeFw5Hl/6w0JBgAAAEqVGjVq6KabbtKqVau0aNEiz/Jz585pypQpnvcul0sZGRk6cOCAMjIyPMtPnjypSZMmScqeTFStWlWStGLFimzlPffcczpy5Ei2ZVlDza5atSrb8tjYWEnSZZdd5tN2f5aWliZJ+u2337It37Jli2bOnJkjdn9CggEAAIBS57nnnlO1atV0++23q2fPnhoxYoRatmypjIwM1apVS2FhYQoNDdVNN92krVu36rLLLtPw4cM1aNAgNW7cWM2bN1flypU1Z84cPf3005LcfYlr1qyp2bNna9CgQZoyZYr69OmjGTNm5Bji9d5775XNZtMNN9ygG264QaNGjVLLli11zz33qH79+ho+fLhP2/1Zly5dVK9ePb3yyitq1aqVRo8erV69eumaa67RrbfeKkl66KGH/HLkVhIMAAAAlDrNmjVTTEyMbr31Vu3Zs0c//PCDBg8erBUrViglJUWRkZGSpPnz5+uvf/2rgoKC9Pnnn+vcuXN6/fXXNW/ePD322GMyDENbt26VJEVERGjVqlXq37+/1q9frwULFqhatWpat26dOnTokK38bt26acWKFerevbu2bNmiRYsWyel06sEHH9T69etVo0YNn7b7s0qVKmnVqlUaPHiwTp48qRUrVig8PFxr167Vq6++qltvvVVHjx7V/v37i+5DLiDDvFQjKh/ExcUpMjKyXE20l9XJxcKPEQAAoFQp7lGkJGn9+vWKiIhQy5Ytsy0/fvy4ateurTFjxmjevHnFFk954+0e2OcajDNnzuj555/X+++/71m2Y8cOtWrVSlFRUapcubLuuusupaSkFCJkAAAAIG/33HOPunbtmmOyuRdeeEGSPM2IUPx8qsE4cOCArrnmGh07dkz33nuvXnnlFUlS27Zt9csvv6hGjRoKDAzUsWPHdOutt+qDDz4ossD9BTUYAAAAxe+DDz7QHXfcoYYNG2rYsGEKDAzUhg0b9NVXX+naa6/VihUrFBho+ZRvyOTtHtinBGPMmDF66623dNddd+nhhx9WmzZttH79enXr1k29e/fWihUr5HQ61adPH23YsEG7du1S06ZNrTsTP0SCAQAAUDKWL1+umTNn6pdfflFiYqLq1KmjW265RU899VS5arJfErzdA/uU1q1du1aXXXaZ5s+f7znosmXLZBiG7r77bgUGBiowMFD33Xef1q9fr59++qnMJxgAAAAoGVkjM8G/+NQH49ixY6pfv3622fu+++47SVLv3r09y6KioiRJhw4dsiJGAAAAAKWETwlG3bp1tWXLFs9EJadPn9amTZvUpk0bVa9e3bPdvn37ZBiGZ7ISAAAAAOWDTwnGNddco+TkZM2cOVOnTp3So48+KpfLpcGDB3u2OXbsmP79739Lkpo3b25psAAAAAD8m0+dvPfv36/LL79c586dk+Tu1FGlShX9+uuvql69uv73v/9p4sSJSk1N1dVXX63169cXWeD+gk7eAAAAKG8smwejYcOG2rRpk4YNG6ZWrVrp+uuv18qVKz3No06dOiWbzaahQ4dqyZIlFoQOAAAAoDSxdCbvlJQUhYSEWHW4UoEaDAAAAJQ3ltVgjBs3TrNnz85zfVZycfjwYY0bN07ff/+9L4cHAAAAUMr5lGDMnz9fq1atuuR2cXFxmj9/vjZs2FDgwEobwzDyfDkcjpIODwAAAPCJw+HI8/7Wm0s2kbr22ms9///tt9+qatWqatu2bZ7bu1wu7dq1SydPntQrr7yie++918dTKV1oIgUAAIDyxts98CUTDJvtj0oOwzDyfSPdqFEj/fDDD2V+LgwSDAAAUBrN2/VzSYfg1dgW7Yq9zLfeekujR4/W6tWr1aNHj2IvvzTxdg8ceKmdY2NjPTs3bNhQPXv21Jtvvul1n4CAAEVFRV2y+gQAAADwF82bN9cDDzygOnXqlHQopdolE4x69ep5/n/06NFq27ZttmUAAABAWdCpUyd16tSppMMo9S6ZYFxs3rx5RRUHAAAAgDLAp1GkJCkhIUELFizQk08+6fX11FNPFUW8AAAAgBo2bKi+ffsqPj5eEyZMUNOmTVW7dm0NHjxYv/zyS47tX3nlFXXr1k3h4eFq1qyZhg8frv3792fb5q233pJhGFqzZo1nWXJysv71r3+pdevWqlixourWratbbrlFu3btyrZvfreT3JNTT5gwQa1atVLlypXVuXNnPfHEE8rIyMi2Xa9evdS0aVOlp6drypQpqlu3ripWrKiOHTvq008/LczHV6R8mmjv999/V+/evXX48OFLdmo2DENOp7PQAfo7OnkDAIDSqLR38m7YsKGqVq0ql8ulY8eOqWfPnjp27JjWrl2rkJAQff75556O2rfddpsWLVqkyy67TN26ddPx48e1Zs0aVaxYUStWrNDVV18tKfdO3jfddJOWLl2qOnXqqFu3bvr999/1008/qUaNGtq+fbuqVavm03a//fabunfvrmPHjqlLly5q3LixfvjhB+3Zs0ddunTR6tWrZbfbJbkTjEOHDumqq67S119/rV69euncuXP64osvJEnr1q3zxF7cCtXJ+2JTp07VoUOH1KhRI91zzz2qWbOmNRECAACg2HRb8m5Jh3BJ83dvlyStHTIiz21++ukndezYUTt27FBkZKQkadmyZRo8eLAefvhhbd68WcuWLdOiRYvUv39/LV68WKGhoZKkVatW6YYbbtCkSZO0cePGXI9/6NAhLV26VP3799fnn3/uuameOXOmJk+erJUrV2r48OH53k6S/v73v+vYsWOaP3++Ro8eLUlyOp2aMGGC5s2bp7lz5+ovf/mLJ4bY2FhVrFhRv/zyi6pXry5JevvttzVq1CgtWbKkxBIMb3xKMNatW6ewsDB9//33qlGjRlHFBAAAAOTLv//9b09yIUkDBw7Ubbfdpg8++ECbN2/Wf//7XwUEBOjll1/2JBeS1LdvXw0bNkzvvvuutmzZovbt2+c49pkzZyRJdrs92+io9957r/r27avatWv7tF1cXJw+/vhj9ezZ05NcSO4RWJ9//nktWrRIr732WrYEw+l06umnn/YkF5J06623atSoUTp58mSBPrOi5lMfjISEBLVr147kAgAAACXObrere/fuOZZff/31kqS9e/dqz549atiwoRo1apRju969e0tyN1vKTcuWLdWuXTstXbpUvXv31ksvvaQtW7YoNDQ02z1xfrfbu3evJKlPnz45yqpSpYrat2+fayx/rqUICQnJ8zPxBz7VYLRq1UoHDx4sqlgAAABQDNYOGVHq+2BIUs2aNbNNCp0lax6LQ4cO6ciRI+rYsWOu+2fVLBw6dCjX9Xa7XWvWrNHTTz+tDz74QH/961895Y4dO1b//Oc/FRISku/tjhw5IkmqVatWnvEkJyfrzJkznlqZgIAARUREXPKz8Cc+1WD885//1OHDhzV//vyiigcAAADIlxMnTsjlcuVYfuzYMUnuG/k6dero+PHjue6ftTwr0chNeHi4nnnmGcXGxmr37t363//+p+bNm+vpp5/O1pQpP9tlJT7e4qlQoYKqVKniWVYaJ672KcG44YYb9Pbbb+vhhx/W6NGj9dlnn+mXX37R3r17c30BAAAARSUtLU1r167NsXzlypWSpGbNmqlp06bav39/jiFpJembb77xbJebTZs26R//+Id27twpSYqOjtbdd9+tr7/+WnXr1tWyZct82q5p06bZyr3Y2bNnFRMToyZNmuRaK1Oa+NREKiAgQIZhyDRNvfPOO3rnnXfy3NYwjBxj+QIAAABWeuSRR7Ry5UpPM6LPPvtM77//vtq2baurrrpK9957r1asWKEHH3xQixYt8vRf+PLLL7VgwQJ17NhRHTp0yPXYZ8+e1TPPPKNDhw7pnXfe8dQmnDx5UufPn1e9evV82q5mzZoaPHiwPvnkE73zzjsaOXKkJHdH7kmTJikpKUn33HNP0X1YxcSnBKNHjx6lspoGAAAAZU9kZKROnjypVq1aeebB+O6771ShQgW9+OKLstlsuummmzw39S1atFDXrl11/PhxrV69WhUrVtR//vOfPO9vu3fvrtatW+u9997Ttm3b1KFDBx08eFAbNmzQhQsX9N///ten7STpmWee0YYNG3TXXXfp9ddfV6NGjbLNg1EWEgyZKBRJJh8jAABA8WrQoIHZpEkTMy4uzrzzzjvNevXqmTVr1jQHDRpkbtu2Ldu2LpfLnD17ttmlSxezcuXKZpMmTcw77rjD3LdvX7bt3nzzTVOSuXr1as+yI0eOmPfcc4/ZuHFjs0KFCmZUVJR5/fXXm6tWrcq2b363M03TPH78uDl27FizRYsWZlhYmHnVVVeZTzzxhJmenp5tu549e5qBgYG5nr8kc/To0T59Zlbydg/s00zeyCk/NTrTpk2Tw+Eo+mAAAADKiYYNGyowMJB+v0XI4XBo+vTpXrfJLZUoUILhcrm0ZMkSrVixQkePHtXRo0e1ZcsWrV27VomJibr++usVEBDg62FLJW/TpAMAAKBokGCULG/3wD71wZDc06qPGTNGx44d8xwwq4AtW7Zo0qRJ6tKli5YsWaJq1aoVJm4AAAAApYxPY2Bt27ZNt9xyi+Lj4+VwOLRjxw5de+21nvWDBw/WwIEDtX79eo0aNcryYAEAAAD4N59qMF544QWdO3dOixcv1pAhQyS5ZzjMUq9ePX3yySfq37+/VqxYoa1bt+ryyy+3NGAAAAAgt3kt4B98qsH48ccfVa9ePU9ykZcJEybINE1t3bq1MLEBAAAAKGV8SjDi4uIUFRV1ye0qVqwoSUpISChYVAAAAABKJZ8SjA4dOmj79u1KTEz0ut0PP/wgwzDUrl27QgUHAAAAoHTxKcEYOHCgzp07p9GjRys5OTnXbbZu3arnnntOVatWVceOHS0JEgAAAEDp4FMn77/+9a9auXKlPvnkEzVr1kwDBw7Uvn37JEnPPvustm7dqo8++khOp1MffvihwsLCiiRoAAAAAP7J54n2UlJS9MILL2jmzJm59rG4/PLL9eyzz6pv376WBenPmGgPAAAA5Y23e+ACzeQtSUlJSfr555+1d+9enTp1Sg0bNlSTJk3K3bC0JBgAAAAobyxLMHbu3KmWLVtaF1kZQIIBAACA8sbbPbBPnbxbt26tq666Si+//LJOnTplTXQAAAAAygyfajAaNWqk2NhYGYahwMBA3XDDDbrrrrs0YMCAbDN6lyfUYAAAAKC8sbQPxk8//aQPP/xQixcv1v79+2UYhqpUqaI77rhDI0eOVKdOnayJupQgwQAAAEB5UySdvCV3srFw4UJ99NFHnmSjadOmGjVqlO68807Vq1ev4FGXEiQYAAAAKG+KLMG42ObNm7Vo0SItXLhQhw4dks1mU3p6uhWH9mtZH64306ZNk8PhKPpgAAAAAIs4HA5Nnz7d6zZFlmAkJyfriy++0JIlS/T5558rISFBhmHI6XQW9tB+jxoMAAAAlDfe7oF9msn7YmfOnNGyZcu0ZMkSrVy5UqmpqTJNU1FRURo1apRuu+22gkcMAAAAoFTyKcE4cuSIPvnkEy1ZskTfffednE6nTNNUzZo1dcstt+i2225T165d89VsCAAAAEDZ41MTqYCAAEnuqpDq1atr6NChuu2229SjR49ym1TQRAoAAADljWVNpCIiIjxJRa9evWSz+TRPHwAAAIAyzqcaDKfT6anFgBs1GAAAAChvvN0De62CWLt2rXbs2OF5n9/kYtasWRo3bpwvMQIAAAAoA7wmGD169NDf//73XNc1btw4zyRi1apVevPNNwsdHAAAAIDS5ZKdKPJq+rN//34dP37c8oAAAAAAlF700gYAAABgmVKRYDidTs2YMUPt2rVTaGiomjZtqpkzZ+Z7/23btunWW29VVFSUKlWqpE6dOmnmzJm5zjQeFhYmwzByfb3++utWnhYAAABQ5hR4Ju/iNGHCBM2bN08NGjTQ4MGDtWHDBk2ePFmpqamaOnWq133Xr1+v3r17y+l06sorr1STJk08+69YsUKrVq3y9II/ceKEzp8/r44dO6pz5845jtWqVasiOT8AAACgrPD7BGPdunWaN2+eevTooVWrVslutys9PV19+vTRY489pv79+6tDhw557j958mSlp6dr8eLFuvnmmyVJ6enp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3601a9ZowYIFio6OVpcuXdSkSRNFRETo6NGj+uGHH3TgwAEFBAToww8/VEhISFHFDQAAAMAP+ZRgSNLbb7+tvn37avr06Vq3bp3WrVuXbf2AAQP0r3/9S23aFN2ETAAAAAD8k88T7dlsNo0aNUq///67Dhw4oG+++UYLFy7Uxo0bderUKS1dutTy5MLpdGrGjBlq166dQkND1bRpU82cOTPf+2/btk233nqroqKiVKlSJXXq1EkzZ86U0+nMsW1KSoomT56sFi1aKDQ0VG3atNHbb79t5ekAAAAAZZbPNRhZDMPQZZddpssuuyzHuri4OC1fvly9e/dW/fr1CxWgJE2YMEHz5s1TgwYNNHjwYG3YsEGTJ09Wamqqpk6d6nXf9evXq3fv3nI6nbryyivVpEkTz/4rVqzQqlWrsk11PmjQIH311Vdq2bKlbrrpJq1evVqjRo2SzWbTiBEjCn0uAAAAQFnmcw1GfixYsEDjx4/X4sWLC32sdevWad68eerRo4f27NmjBQsWaM+ePerevbsee+wxxcTEeN1/8uTJSk9P14cffqgNGzbonXfe0a5duzRy5Eh9/fXXmjNnjmfb9957T1999ZWGDx+uHTt26P3339fu3bvVvHlzjRs3TnFxcYU+HwAAAKAs8znBmDVrlpo1a6aAgIA8X4888ogkqUWLFoUOcP78+Z5y7Xa7JMlut2v27NmS5DWJOX36tDZu3Khu3brp5ptv9iy32+166aWXZLPZtHz58mxl2Ww2vfjii55l4eHhmjFjhtLS0vTpp58W+nwAAACAssynBOP999/Xww8/rN9++021atWSYRgyTVO1a9dW/fr1ValSJZmmqYiICD300EPq169foQNcv369oqKicsyp0b59e0VFRWnp0qV57nvgwAGZpqlOnTrlWBceHq4qVapo7969kiSXy6WNGzeqc+fOqlatWrZt+/btq6CgIK9lAQAAAPAxwXj99ddlGIY+//xzHTlyRDExMQoICNDLL7+sffv26cyZM3rhhRd0/vx53XTTTQoMLHAXD0lSRkaG9u7dq+bNm+e6Pjo6WrGxsXnu37hxY61evVr3339/jnU///yzzpw5ozp16kiSYmNjlZycnGtZYWFhqlOnjteyDMMo8AsAAAAobkV1/+pTgpF1s9+/f39JUtu2bdWqVSt9++237oPZbHrooYc0YMAAjR49Wunp6QU8XbfExES5XC5FRETkuj4yMlLnz59XSkpKruvDw8PVo0ePHB3Nz549q/vuu0+SNHz4cM8ySV7LOnXqVEFOAwAAACg3fEowTp8+rVq1amVb1rJlS/3888/Zlg0dOlQHDhzQ5s2bCxVcVuIQFhaW6/qs5fHx8fk+5qZNm9S1a1d9//336tevn2dkqPyUdebMmTyPa5pmgV8AAABAcSuq+1efEoxq1arp0KFD2ZY1a9ZM27ZtU0ZGhmdZeHi4TNPUpk2bfDl8DpGRkZKkpKSkXNcnJCRIkqpUqXLJYyUlJWnChAnq1KmTduzYoWHDhumjjz7yNOPKT1l51W4AAAAAcPMpwejevbt+//33bEO7XnXVVUpISNDHH3/sWZY1t0TlypULFVxISIgqVaqUZ81BfHy8KleurNDQUK/H2bRpky6//HLNmTNHdevW1Ycffqj3338/W21FzZo1JclrWX+uvQEAAACQnU+9sP/2t79p6dKluvfee7V06VItW7ZMffr0UVhYmO6//37t2bNH586d02uvvabAwEB179690AFGRUVp165dMk0zW4cS0zS1e/duRUVFed1/586d6tu3r5KSkvS3v/1NTz75ZK4JSWRkpCpUqKCdO3fmWJeYmKgjR45YMuwuAAAAUJb5VIPRsWNHrVq1SrfccounWVJQUJDmzZun+Ph4Pf7443ruueeUlpamGTNmqFGjRoUOcODAgYqLi8sxoV5MTIzi4uI0YMAAr/uPGDFCSUlJ+vDDD/X88897re0YMGCANm/erBMnTmRbvmrVKmVkZFyyLAAAAKC8M0yLehnv27dPa9euVXJysrp166bWrVtbcVjt2LFDrVu3Vrdu3fT111/LbrcrPT1d1157rdauXaudO3fmWbPwww8/qHPnzho1apTefPPNS5b1+eefa8CAARo2bJjef/99Se6+F506ddKBAwd09OjRHP0wsmpV6KwNoKzqtuRdSdLaISNKdRkAAOt4uwf2qYnUsmXLVL16dXXu3DnHukaNGllSY/FnrVq10pQpU/TMM88oOjpanTt31oYNGxQbG6upU6dmSy4efPBBSdL06dMVGRmpDRs2SJIOHjzoWZdb3JMmTZIk9e/fX8OHD9eCBQu0fft2tWnTRqtXr1ZcXJxee+01OnkDAAAAl+BTDUZISIhat25d6NGhCuKll17SokWL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",
+      "text/plain": [
+       "<Figure size 799.992x599.976 with 1 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "plotter.draw(\"x\", show_error=True, stacked=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "bee2a551-140a-4c0a-8f07-ff29ff91b0d9",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[INFO] Using deduced bin range (0.008, 99.992)\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<Axes: ylabel='Fraction of Events / 4.00'>"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "<Figure size 640x480 with 0 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "image/png": "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i4vLd3GnTp2Sw+FQdHS0x/kxMTE6e/as0tPTFR4enu/95GbOnDk6dOiQ/vWvf+WYl5iYKMnZGtS+fXsNHDhQW7du1dy5c7VkyRL98MMPub737E48+VGSky0AAAD8U0HOX73xGkZWrlyp6OhovfbaawoKuuwowJKktm3basKECXr44Ye1YsUKjRkzJt/FpaenS5LKlSvncX729JSUlEIPIxkZGZoxY4ZuuOEGdejQIcf85ORkVatWTa+99poGDhzomv7888/r4Ycf1rhx47R48eJCrQkAAMAqaZ/MleP4EavLyFVQ5eqKuG10se1vwYIFGjFihL755hvXg74dDofGjh2rjz/+WDVr1tTWrVuLrZ7SymsYOXXqlBo0aJDnIJKtUaNGkuR6EGJ+xcTESJJOnz7tcX5qaqokqWLFigXajyfvvvuuDh06pDfeeMPj/M8//9zj9IceekgffPCBli5dqoyMDI+jjtG6AQAAShvH8SMKqnKF1WXkynHscLHur3Hjxho7dqxq1KjhmrZo0SK9/vrratSoUY5HPJR2BTl/9daq4jWMNG3aVD/++KMOHjyomjVr5nmHn332mQzDUP369fNepQfh4eEqX768kpOTPc5PSUlRVFSUIiIiCrQfT+bPn69KlSqpR48ePq/bvn17bdmyRbt371bz5s0LvTYAAABY65prrtE111zjNm3nzp2SpH/9619q2bKlFWWVOl6bPIYNGya73a727dtr+fLlysrK8rqxffv2acyYMXr77bdVo0YN3XzzzQUuMDY2Vjt27MiRxkzT1M6dOxUbG1vgffzZ7t27tXHjRg0ePNg1nPCf2e12ORwOj/Oy+9ZUqFCh0GsDAABAyZR9vso5YN55DSP333+/xowZo0OHDqlPnz6uloI777xT48aN04QJE3TPPfdo0KBBatq0qerVq6e5c+eqWrVqWrx4caE8GLFPnz5KSkpSQkKC2/SEhAQlJSWpd+/eBd7Hn3344YeSpFtuucXj/MTERIWEhKh///4e52/atElRUVG68sorC702AAAA+Obuu+/O9VahOnXq6LrrrpMkTZ06VYZh6NChQ1qwYIGaNm2q8PBwNWjQQDNmzHC7EL1gwQIZhqG1a9e6tmOz2SRJtWvXVp06ddz288knn6h79+6qVKmSateurf79+2vLli1uy2Rvc8OGDXr99dd1xRVXaMCAAW617d+/X9OmTdPVV1+tSpUqqU+fPtq+fbvS0tI0YcIEtWjRQuXKldM111yj//73v4Xy+RWlyz5n5I033tDAgQP1yiuv6D//+Y9WrVqV67L169fXbbfdpvHjx7v6exTUiBEjNHPmTI0fP16rV692PYF9/PjxkqSRI0cWyn4utWrVKgUHB6tdu3Ye59epU0cdO3bUsmXLtHTpUvXp08c179VXX9V3332nxx57rNDrAgAAQNF788039dxzz6lbt26Kj4/XF198occee0xBQUF65JFHPK4zfPhwrVixQps2bdLw4cNVu3Zt17wJEyZo5syZqlq1qrp3767Tp09r5cqV+vLLL/Xxxx+7Ake2efPm6Z133tH111+v66+/3m3e3/72N/3444+6/vrrZZqmvvjiC23btk0NGjTQ1q1b1b17d0VGRur7779Xv379tGfPnkI7Ly8KeXoC+4033qgbb7xRdrtdO3fu1MGDB3X69GllZWWpatWqqlatmqpXr65KlSoVeoHNmjXTxIkTNWPGDDVq1Ejt2rXTxo0btW/fPk2aNElNmjRxLfvAAw9IcibH/H7oZ8+e1aZNm9SiRQtFRkbmutyLL76oHj16qF+/furatatq1qypbdu2acuWLWrTpo3b09oBAABKs+Rbr7W6hDxJXrtCMf9aX+DtvPTSS9qwYYNat24tSdq1a5datmypRYsW5RpGpk6dqqCgIG3atElPPvmkatWqJUn6+eef9cILL+jqq6/WihUrVLlyZUnSli1b1K1bNz344IPq1auX2x1F7733nr799tscfVIkZ7+UX375RTExMXI4HGrUqJH+97//KSgoSL/88ovrWXd9+/bV0qVLtWnTJt10000F/kyKik/DZAUHB6tZs2bq0aOHbrnlFt1+++3q1q2bmjVrViRBJNszzzyjl156STVr1tTy5csVFxenV199VdOmTXNb7pVXXtErr7yS6+hbebF+/XplZmbm2iqS7eqrr9b333+vQYMG6ddff9Wnn36q0NBQPfnkk/r222+LpFM9AAAAit6YMWNcQUSSGjZsqFatWunYsWM+b+vNN9+Uw+HQ888/7woikhQfH6+xY8fqwIEDWr58uds6Q4cO9RhEJGcrS/ZF96CgIFeXhYcfftjtods9e/aUJJ04ccLnmotTnlpGSoIHHnjA1fKRm8sNOVarVq3LLtOjR488D11Wr149LVy4ME/LAgAAoHRo3759jmn5vdC8a9cuhYWFqVOnTjnmdevWTdOmTdP//vc/t+nNmjXLdXsNGzZ0+zk74GQ/WqOg9Ra3Igkjd999t9atW5fjgwUAAEDpE/Ov9Trz6rQS/5yRcvf7fpu8p4vQl7YwFNShQ4dUtWpVj8/tu+IK5+f5+++/u02vXr16rtsLDg72aXpJ59vTDPPoyJEjSkxMLIpNAwAAAIXG021M3h7S56saNWro6NGjHh8JceSI84n22aEkm68PHC/NiuSdPvzww5o/f35RbBoAAADIl4yMDLefd+zYoTNnzhTpPhs0aKDz589rw4YNOeatWbNGUs5brwJJkYSR66+/XsOHDy+KTQMAAAA+yR5oacWKFa5p58+fz3VkrMI0ZswYGYahhx56SMnJya7pCQkJrgGaevXqVeR1lFSlpgM7AAAAkB8DBgzQrFmzdPvtt2vo0KGqWLGivvrqKzkcjsuOoFpQ8fHxGjdunGbPnq2mTZuqa9euOn36tNasWSOHw6H58+crPDy8SGsoyQoljEycOFE//vij/vOf/xTG5gAAAFDCBFWuLsexw1aXkaugyrl3+u7YsaM+/fRTPf300/rkk09Uu3Ztde3aVVOnTtWwYcN07ty5Iq1t1qxZatOmjebOnatVq1apXLly6tGjh2w2m6666qoi3XdJZ5h5HcfWi169eumrr76S3W4vjJr8Vl46Q02ZMkU2m63oiwEAAAAKic1m09SpU70u4yl2eA0jc+bMydPOX3zxRe3YsUNvvvmm207uueeePK0fKLLDSCHkPwAAAKBU8HYO7DWMBAUFuV3NN00zT1f3s5ejpcQdYQQAAACBxts5sNc+I++9957Gjh2r1NRUNW7cWLfddpvHMPL+++9rz549mjJlSiGVDAAAAMDfXbbPyIEDBzRs2DCtX79eAwcO1FtvvaWYmBi3Zegzkje0jAAAACDQeDsHvuxzRuLi4rR27Vo99dRTWrp0qVq2bKmVK1cWfpUAAAAAAkqeHnpoGIYmTZqkDRs2KCIiQj179tSDDz6Y4ymWAAAAAJBXPj2BvU2bNvrpp580cuRIvfTSS7r66qv1008/FVFpAAAAAPxZvp8z8tlnn2nMmDE6deqUoqKidOLECfqMXAZ9RgAAABBo8j207+UcOXJEd911l1atWsVQvnlAGAEAAECgKbIwku3UqVOSpKioqIJuyq8RRgAAABBoijyMIG8IIwAAAFKd0XMlSYlzR1tcCYpDgYb2BQAAAICiQBgBAAAAYAnCCAAAAFCKrF27VoZhaMGCBVaXUmCEEQsYhpHry2azWV0eAAAASrAaNWpo7Nixaty4sdWluNhstlzPb73x2oF98+bNuvrqqwu92EBFB3YAAAA6sAeafHdg79q1q+rUqaOHH35Y3333XdFUBwAAACAgeQ0jx44d0wsvvKAjR47opptuUs2aNTVu3Dj997//5eo+AAAASo1jx45p1KhRql+/vmrWrKnhw4crKSlJDRs2VIMGDVzL/fLLLxoyZIiuuOIKlSlTRjVq1FCfPn307bffum1vwYIFMgxDa9euzbGvu+++O8ftSb/++quGDBmiWrVqKSIiQo0bN9bUqVOVnp7u83K59RlZsmSJunXrpkqVKik8PFx169bVmDFjdODAgRz1hYaGyjRNzZw5U3Xr1lVERISaN2+uuXPn+vbBFlCIt5lly5bVgAEDNGDAAJ0/f14rV67Uv//9bw0YMEChoaEaMGCAbrnlFl133XUKCqL7CQAAAEqeAwcOqHPnzq4/a9SooTVr1qhr164KCwtTRkaGJGnfvn267rrrlJqaqu7du+vKK6/UwYMH9dVXX2n16tVKSEjIVz+NvXv36tprr1Vqaqpat26tDh06aN26dbLZbDp48KDmzJnj03KefP755xo4cKDKly+vHj16qHz58tq2bZvmzJmjr7/+Wjt27FBIiPup/6RJk/Tqq6/q+uuvV1BQkL744gvdc889ioqK0q233urz+8wPr2HkUmFhYerdu7d69+4tu92u1atX69NPP9XQoUNlt9vVv39/DRo0SN27d1doaGhR1gwAAIBilN3HozRs11M/lClTpmj//v366KOPdPvtt0uSTp06pZ49e+rbb79V/fr1JUlffPGFTpw4oU8++cTtZPyTTz7R7bffrhUrVuQrjHzwwQdKSUlx267dblfr1q31/vvv64033lBwcHCel/PkvffeU5kyZfTzzz+rVq1arul//etf9cYbb2jbtm266qqrXNOzsrL0wQcfaMuWLapbt64kZ4tL165dtWjRomILI/lqzggODtaNN96oN998U4cPH9bChQsVHh6u0aNHq2rVqho+fLiWLFlS2LUCAAAAPklJSdF7772n66+/3hVEJCkqKkozZsxwW/baa6/VokWLNGDAALfp2QEkNTU1XzUkJydLktsF++DgYC1dulTfffedq/tDXpfz5N5779WSJUvcgsjlan/00UddQUSSunTpoqpVq+rYsWO+vsV8y3PLSG6CgoJ03XXX6brrrtPLL7+sDRs2aNGiRRo3bpz69u1bGDUCAAAA+bJr1y7Z7XZ169Ytx7wOHTq4tTRcddVVuuqqq2Sapvbv36/du3frt99+07vvvlugGm699Va9/PLLuvPOOzV06FB1795dHTt2VFxcnOLi4nxezpMbbrhBkpSZmak9e/Zo9+7d2r59u15++eVc12nfvn2OaREREfl8l/lT4DDyZx07dlTHjh01a9aswt40AAAALFDYQ/AW59C++/fvlyRVq1Ytx7zg4GBVqVLF9XNaWpoee+wxLVq0SH/88Yeio6PVokULXX311dq0aVOe9/nnFoz27dvru+++0/Tp0/XBBx+4+n60adNG//jHP9SnTx+flvMkMTFREyZM0MqVK3X69GnVqFFD8fHxatu2rT777DOP61z63q1Cr3MAAAD4rSuuuEKSlJSUlGOew+HQiRMnXD/fdtttevXVVzVq1Cjt3btXycnJWrt2rR599FGf9nnpNrO1adNG//73v13bnDJlig4dOqS+ffvq66+/9nm5S6Wnp6tdu3Zat26dZs6cqeTkZB08eFBLly5V//79c63zcg8kLA6EEQAAAPitRo0aSZK++eabHPO+++47ZWZmSpLOnTunlStXqnfv3nryySdVp04d13JHjhzJdfvnzp1z+zkrK0tbtmxxm/bGG29o6tSpkqQyZcqoc+fOstlseueddyQ5O877spyn93H06FE99thjGjNmjKKjo/NUe0lAGAEAAIDfqlq1qvr166dVq1Zp4cKFrulnzpzRxIkTXT87HA5lZWVp//79ysrKck0/duyYxo8fL8k9eFSqVEmStGLFCrf9Pfvsszp06JDbtOzheVetWuU2fd++fZKkK6+80qfl/uz8+fOSpP/9739u07ds2aKZM2fmqL0kIYwAAADArz377LOqXLmybrvtNnXt2lXDhg1T06ZNlZWVperVq6tcuXKKiIhQv3799NNPP+nKK6/U0KFD1bdvX9WrV0+NGzdWVFSU5syZo2eeeUaSs590tWrVNHv2bPXt21cTJ05U9+7dNX369BzD4v7lL39RUFCQevXqpV69eumuu+5S06ZNde+996pWrVoaOnSoT8v9WYcOHRQXF6dXX31VzZo104gRI3TdddepY8eOGjx4sCTpwQcfLJGj3RJGAAAA4NcaNmyohIQEDR48WLt27dL333+v/v37a8WKFUpPT1dMTIwkaf78+frb3/6msLAwLVu2TGfOnNGbb76pefPmafLkyTIMQz/99JMkKTo6WqtWrVLPnj21YcMGffjhh6pcubLWr1+v1q1bu+2/U6dOWrFihTp37qwtW7Zo4cKFstvteuCBB7RhwwZVrVrVp+X+rHz58lq1apX69++vY8eOacWKFapQoYLWrVun1157TYMHD9Yff/yhxMTEovuQ88kwvQ1Y7KOkpCTFxMTw0MNcZHcSKsSPHAAAoNQpztG0JGnDhg2Kjo5W06ZN3aYfOXJEV1xxhe6++27NmzevWGoJRN7OgX1uGUlOTtbzzz+vjz76yDVt+/btatasmWJjYxUVFaXhw4crPT29ACUDAAAAhePee+/Vtddem+PBfy+88IIkuW5lQvHzqWVk//796tixow4fPqy//OUvevXVVyVJLVu21LZt21S1alWFhITo8OHDGjx4sD7++OMiK7w0ysvwaVOmTJHNZiv6YgAAAALExx9/rCFDhqhOnTq6/fbbFRISoo0bN+o///mPrr/+eq1YsUIhIYX++L2AYrPZXCOB5cZT7PApjNx9991asGCBhg8froceekgtWrTQhg0b1KlTJ3Xr1k0rVqyQ3W5X9+7dtXHjRu3YsUMNGjTw/d34KW7TAgAAsMby5cs1c+ZMbdu2TadOnVKNGjV0yy23aNq0aXQxKGLezoF9ioDr1q3TlVdeqfnz57s2unTpUhmGoXvuuUchISEKCQnRfffdpw0bNmjz5s2EEQAAAFgue4QqlCw+9Rk5fPiwatWq5Xa70X//+19JUrdu3VzTYmNjJUm///57YdQIAAAAwA/5FEZq1qypLVu2uB4Ec+LECf3www9q0aKFqlSp4lpu7969MgzD9TAYAAAAAPgzn8JIx44dlZaWppkzZ+r48eN65JFH5HA41L9/f9cyhw8f1nPPPSdJaty4caEWCwAAAMB/+NSBPTExUVdddZXOnDkjydkJpWLFivrtt99UpUoVvfXWWxo3bpwyMjLUvn17bdiwocgKL43owA4AAIBAU2jPGalTp45++OEH3X777WrW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+koffvih2rRpo9atW3vc9smTJzVjxgxNnz7dbQjbY8eO6ezZs7ryyit9Wq5atWrq37+/1qxZo/fee8+1nN1u1/jx43X69Gnde++9hfPBWMinPiNdunSxpPkne/iy2rVrq3///tq4caMmTJigjIwMTZo0Kc/bSUxM1MqVK3XnnXd6nH/06FGdPXtWbdq0Ubt27XLMb9asWZHUBQAAgKIVExOjY8eOqVmzZq7njPz3v/9V2bJl9eKLLyooKEj9+vVT//799dlnn6lJkya69tprdeTIEX3zzTeKjIzUSy+9lOu5cOfOndW8eXN98MEH2rp1q1q3bq0DBw5o48aNOnfunF5//XWflpOkGTNmaOPGjRo+fLjefPNN1a1b1+05I/4QRmSWcOvWrTMlmV26dDHPnz9vmqZpnj9/3uzcubMpydy8ebPX9R0Oh7lnzx5zwYIFZoMGDUxJ5ujRoz0uu3HjRlOS+corrxRJXZLMUvCRAwAA+JXatWub9evXN5OSksw77rjDjIuLM6tVq2b27dvX3Lp1q9uyDofDnD17ttmhQwczKirKrF+/vjlkyBBz7969bsu98847piTzm2++cU07dOiQee+995r16tUzy5Yta8bGxpo33XSTuWrVKrd187qcaZrmkSNHzJEjR5pNmjQxy5UrZ7Zt29Z88sknzczMTLflunbtaoaEhHh8/5LMESNG+PSZFSZv58A+PYHdCqNGjdK8efOUkJDgNqbzli1b1Lp1az322GOaPn16ruuvXr06x5Boo0eP1pw5c3Is++GHH+qOO+7Q8uXL1bNnz0KviyewAwAAFL86deooJCSEPs0W8XYOnK8eLw6HQ59++qnuuece3Xzzza6T8XXr1mnZsmWy2+0FKNfdhg0bFBsbm+PhMvHx8YqNjdWSJUu8rt+iRQstWrRIixYt0muvveZ12b1790qS6tWrV+R1AQAAAIHOpz4jkrRq1SrdfffdOnz4sCvdZKedLVu2aPz48erQoYMWL16sypUrF6i4rKws7d69W127dvU4v1GjRtq0aZPXbVStWlWDBg2SJO3fv9/rstlh5Msvv9Qtt9yivXv3qm7durrxxhs1depURUZGFlpdAAAAQKDzqWVk69atuuWWW5SSkiKbzabt27fr+uuvd83v37+/+vTpow0bNuiuu+4qcHGnTp2Sw+HI9eEtMTExOnv2rNto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Vqz9RcaMGaN58+apdu3a6t+/vzZu3KgJEyYoIyNDkyZNyvN2EhMTtXLlSt15550e5+/evVtt27ZVamqqWrVqpa5du2rLli2aPn26Pv/8c/3www8KDw93Lb9v3z41bNhQN954Y45ttW/f3vc3CgAAAAQS0wenTp0yr776arN9+/bm7t27fVk139atW2dKMrt06WKeP3/eNE3TPH/+vNm5c2dTkrl582av6zscDnPPnj3mggULzAYNGpiSzNGjR3tcdsiQIaYkc/bs2W7rT5w40ZRkPvbYY67phw8fNiWZDz/8cJ7fiyTTx48cAAAAKNW8nQP71DIyc+ZMdevWTW+88YaaNm2qli1bqnbt2ipTpkyOZQ3D0Pvvv1/AqCTNnz9fkjRr1iyFhoZKkkJDQzV79my1bt1aixYtUuvWrXNdf82aNerevftl92OappYvX664uDiNGzfONd0wDE2bNk3z5s3T8uXLNX36dEnS3r17JUl169bN93sDAAAAAplPYeSpp56SYRiuzicJCQlKSEjwuGxhhZENGzYoNjZW8fHxbtPj4+MVGxurJUuWuAKCJy1atNCiRYskSUePHtV9993ncbmUlBSlpqZ6vOUqODg4R8f97DBSr149n98TAAAAAB/DyDfffFNEZXiWlZWl3bt3q2vXrh7nN2rUSJs2bfK6japVq2rQoEGSpP379+e6XGRkpL755htVr149x7wTJ07o559/dmsFyQ4jmzdv1uTJk7Vjxw7VqFFDnTp10vTp0732qSnIkMgmI3EBAACgmBXVIz18CiOdO3cukiJyc+rUKTkcDkVHR3ucHxMTo7Nnzyo9Pd2tY3l+lClTRl26dMkxPSMjQ/fcc4/sdruGDh3qmp6YmChJmjx5stq3b6+BAwdq69atmjt3rpYsWaIffvhBcXFxBaoJAAAA8Gc+De37Z+fOndOOHTu0YcMGHTp0qLBqcskeTrdcuXIe52dPT0lJKfR9S9Jvv/2mG264QYsXL1bLli318MMPu+YlJyerWrVqWrhwodavX68FCxZoy5Ytmjlzpo4ePerW7+TPTNPM9wsAAAAobkV1/pqvMLJw4UK1bNlSkZGRat68uTp37qy4uDhFREToL3/5i/744498vck/i4mJkSSdPn3a4/zU1FRJKvRnn5w/f16PP/64WrRooXXr1un666/XypUr3ULR559/riNH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AAAAAJYgjAAAAACwBGEEAAAAgCUIIwAAAAAsQRgBAAAAYAnCCAAAAABLEEYsYBhGri+bzWZ1eQAAAIBPbDZbrue33vDQQwuYpml1CQAAAEChsdlsuV5U9xZIaBkBAAAAYAnCCAAAAABLEEYAAAAAWIIwAgAAAMAShBEAAAAAliCMAAAAALBEqQgjdrtd06dPV6tWrRQREaEGDRpo5syZeV7/xIkTuvfee1W/fn1FRkaqbdu2WrZsmdsya9eu9fr8j+zX2rVrXes0a9Ys1+Uee+yxQnv/AAAAgD8qFc8ZGTNmjObNm6fatWurf//+2rhxoyZMmKCMjAxNmjTJ67rp6enq0qWLtm/frjZt2ig+Pl6rV6/WwIEDtWLFCl133XWSpBo1amjs2LG5bmfZsmU6ePCgrrzySte0ffv2qWHDhrrxxhtzLN++fft8vlsAAAAgMJT4MLJ+/XrNmzdPXbp00apVqxQaGqrMzEx1795dkydPVs+ePdW6detc13/++ee1fft2Pf7443r66aclSQcOHFDbtm01ePBgHT58WKGhoapfv75efvllj9v47bff9NZbb+nRRx9V3bp1JUlHjhxRWlqa+vbtq+eee67w3zgAAADg50r8bVrz58+XJM2aNUuhoaGSpNDQUM2ePVuStGjRosuuHxMToyeffNI1LS4uTo8++qhOnDihr7/++rI13H///apdu7YmT57smrZ3715JcoUTAAAAAL4p8WFkw4YNio2NVXx8vNv0+Ph4xcbGasmSJbmue+TIEe3du1c33XSTgoOD3ebdfPPNkuR1fUn68MMPtXr1ar355psqU6aMa3p2GKlXr55P7wcAAACAU4kOI1lZWdq9e7caN27scX6jRo20b9++XNf/9ddfJcnj+g0aNFBQUJDX9TMyMvTII4/opptuUteuXd3mZYeRzZs3q23btipfvrwaN26se+65R8eOHfP+xgAAAACU7DBy6tQpORwORUdHe5wfExOjs2fPKj093eP8kydPSpLH9Q3DUHR0tI4fP57r/ufMmaNDhw7piSeeyDEvMTFRkjR58mSFhYVp4MCBCg8P19y5c9W8eXMdOHAg1+3mZdSu3F4AAABAcSuq89cSHUayQ0a5cuU8zs+enpKSku/1k5OTPc7LyMjQjBkzdMMNN6hDhw455icnJ6tatWpauHCh1q9frwULFmjLli2a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",
+      "text/plain": [
+       "<Figure size 799.992x599.976 with 1 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "plotter.draw(\"x\", show_error=True, stacked=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "09d6ea30-c91c-413b-aacd-b5f6b4fd63d9",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}