docs: def box(df, s=None, title_from=None, subplots=False, figsize=(18,6), groups=None, fcol=None, ecol=None, hatch=None, ylabel="", xlabel=""):
      """
      Generate a box plot from pandas DataFrame with sample grouping.
  
      Plot group mean, median and deviations for specific values (proteins) in the dataset. Plotting is controlled via
      the `s` param, which is used as a search string along the y-axis. All matching values will be returned and plotted.
      Multiple search values can be provided as a `list` of `str` and these will be searched as an `and` query.
  
      Box fill and edge colors can be controlled on a full-index basis by passing a `dict` of indexer:color to
      `fcol` and `ecol` respectively. Box hatching can be controlled by passing a `dict` of indexer:hatch to `hatch`.
  
  
      :param df: Pandas `DataFrame`
      :param s: `str` search y-axis for matching values (case-insensitive)
      :param title_from: `list` of `str` of index levels to generate title from
      :param subplots: `bool` use subplots to separate plot groups
      :param figsize: `tuple` of `int` size of resulting figure
      :param groups:
      :param fcol: `dict` of `str` indexer:color where color is hex value or matplotlib color code
      :param ecol: `dict` of `str` indexer:color where color is hex value or matplotlib color code
      :param hatch: `dict` of `str` indexer:hatch where hatch is matplotlib hatch descriptor
      :param ylabel: `str` ylabel for boxplot
      :param xlabel: `str` xlabel for boxplot
      :return: `list` of `Figure`
      """
  
      df = df.copy()
  
      if type(s) == str:
          s = [s]
  
      if title_from is None:
          title_from = list(df.index.names)
          
      if groups is None:
          groups = list( set( df.columns.get_level_values(0) ) )
          
      # Build the combined name/info string using label_from; replace the index
      title_idxs = get_index_list( df.index.names, title_from )
      df.index = [build_combined_label(r, title_idxs) for r in df.index.values]
      
      if s:
          # Filter the table on the match string (s)
          df = df.iloc[ [all([str(si).lower() in l.lower() for si in s]) for l in df.index.values] ]
      
      figures = []
      # Iterate each matching row, building the correct structure dataframe
      for ix in range(df.shape[0]):
          
          dfi = pd.DataFrame(df.iloc[ix]).T
          label = dfi.index.values[0]
          dfi = process.fold_columns_to_rows(dfi, levels_from=len(df.columns.names)-1)
  
          if subplots:
              gs = gridspec.GridSpec(1, len(subplots), width_ratios=[dfi[sp].shape[1] for sp in subplots])
              subplotl = subplots
          elif isinstance(dfi.columns, pd.MultiIndex) and len(dfi.columns.levels) > 1:
              subplotl = dfi.columns.levels[0]
              gs = gridspec.GridSpec(1, len(subplotl), width_ratios=[dfi[sp].shape[1] for sp in subplotl])
          else:
              # Subplots
              subplotl = [None]
              gs =  gridspec.GridSpec(1, 1) 
  
  
          first_ax = None
  
          fig = plt.figure(figsize=figsize)
  
          for n, sp in enumerate(subplotl):
  
              if sp is None:
                  dfp = dfi
              else:
                  dfp = dfi[sp]
  
              ax = fig.add_subplot(gs[n], sharey=first_ax)
              
              #print(dfp.median(axis=1, level=0).reset_index())
  
              medians = dfp.median(axis=1, level=0).reset_index()#.set_index('Replicate') #.dropna(axis=1)
  
              if groups and all([g in medians.columns.get_level_values(0) for g in groups]):
                  medians = medians[ groups ]
  
              ax, dic = medians.plot(
                  kind='box', 
                  return_type = 'both',
                  patch_artist=True,
                  ax = ax,
              )
  
              ax.set_xlabel('')
              for n, c in enumerate(medians.columns.values):
                  if sp is None:
                      hier = []
                  else:
                      hier = [sp]  
                  if type(c) == tuple:
                      hier.extend(c)
                  else:
                      hier.append(c)
                      
                  if fcol:
                      color = hierarchical_match(fcol, hier, None)
                      if color:
                          dic['boxes'][n].set_color( color )
                  if ecol:
                      color = hierarchical_match(ecol, hier, None)
                      if color:
                          dic['boxes'][n].set_edgecolor( color )
                  if hatch:
                      dic['boxes'][n].set_hatch( hierarchical_match(hatch, hier, '') )
  
              ax.set_xlabel(xlabel)
              ax.tick_params(axis='both', which='major', labelsize=12)
  
              if first_ax is None:
                  first_ax = ax
              else:
                  for yl in ax.get_yticklabels():
                      yl.set_visible(False)
  
          first_ax.set_ylabel(ylabel, fontsize=14)
          fig.subplots_adjust(wspace=0.05)
  
          fig.suptitle(label)
          
          figures.append(fig)
          
      return figures