docs: def rankintensity(df, colors=None, labels_from='Protein names', number_of_annotations=3, show_go_enrichment=False, go_ids_from=None, go_enrichment='function', go_max_labels=8, go_fdr=None, progress_callback=None):
      """
      Rank intensity plot, showing intensity order vs. raw intensity value S curve.
  
      Generates a plot showing detected protein intensity plotted against protein intensity rank. A series of colors
      can be provided to segment the S curve into regions. Gene ontology enrichments (as calculated via `analysis.go_enrichment`)
      can be overlaid on the output. Note that since the ranking reflects simple abundance there is little meaning to enrichment
      (FDR will remove most if not all items) and it is best considered an annotation of the 'types' of proteins in that region.
  
      :param df: Pands DataFrame
      :param colors: `list` of colors to segment the plot into
      :param labels_from: Take labels from this column
      :param number_of_annotations: Number of protein annotations at each tip
      :param show_go_enrichment: Overlay plot with GO enrichment terms
      :param go_ids_from: Get IDs for GO enrichment from this column
      :param go_enrichment: Type of GO enrichment to show
      :param go_max_labels: Maximum number of GO enrichment labels per segment
      :param go_fdr: FDR cutoff to apply to the GO enrichment terms
      :return: matplotlib Axes
      """
  
      fig = plt.figure(figsize=(8,8))
      ax = fig.add_subplot(1,1,1)
  
      labels = df[labels_from].values
  
      if go_ids_from:
          ids = df[go_ids_from]
      else:
          if 'Proteins' in df.columns.values:
              ids = df['Proteins']
          elif 'Protein IDs' in df.columns.values:
              ids = df['Protein IDs']
          else:
              ids = None
  
      y = np.log10(df['Intensity'].values)
  
      filt = ~np.array( np.isnan(y) | np.isinf(y) )
      y = y[filt]
      labels = labels[filt]
      if ids is not None:
          ids = ids[filt]
  
      sort = np.argsort(y)
      y = y[sort]
      labels = labels[sort]
      if ids is not None:
          ids = ids[sort]
  
      x_max = y.shape[0]
      y_min, y_max = np.min(y), np.max(y)
      yrange = y_max-y_min
  
      x = np.arange(x_max)
  
      ax.set_xlim( (-x_max//3,  x_max+x_max//3) )
      ax.set_ylim( (y_min-1,  y_max+1) )
  
      # Set the dimensions so we can plot the labels correctly.
      ax.scatter(x, y, alpha=0, zorder=-1)
  
      ax.set_ylabel("Total intensity ($log_{10}$)")
      ax.set_xlabel("Ranked phosphoproteins")
  
      # Defines ranges over which text can be slotted in (avoid y overlapping)
      slot_size = 0.03 # FIXME: We should calculate this
      text_y_slots = np.arange(0.1, 0.95, slot_size)
  
  
      text_y_cross = 0.55
      # Build set of standard x offsets at defined y data points
      # For each y slot, find the (nearest) data y, therefore the x
      text_x_slots = []
      inv =  ax.transLimits.inverted()
      for ys in text_y_slots:
          _, yd = inv.transform( (0, ys) )
          text_x_slots.append( ax.transLimits.transform( (x[find_nearest_idx(y, yd)], 0 ) )[0] )
      text_x_slots = np.array(text_x_slots)
  
      text_x_slots[text_y_slots < text_y_cross] += 0.15
      text_x_slots[text_y_slots > text_y_cross] -= 0.15
  
      def annotate_obj(ax, n, labels, xs, ys, idx, yd, ha):
          """
          """
  
          ni = 1
          previous = {}
          for l, xi, yi in zip(labels, xs, ys):
              if type(l) == str:
                  l = get_shortstr(l)
  
                  if l in previous: # Use previous text slot for annotation; skip annotation part
                      axf, ayf = previous[l]
  
                  elif text_y_slots[idx]:
                      axf = text_x_slots[idx]
                      ayf = text_y_slots[idx]
                      text_x_slots[idx] = np.nan
                      text_y_slots[idx] = np.nan
  
                      ax.text(axf, ayf, l, transform=ax.transAxes,  ha=ha, va='center', color='k')
  
                      idx += yd
                      ni += 1
  
                      previous[l] = (axf, ayf)
  
                  ax.annotate("", xy=(xi, yi), xycoords='data',  xytext=(axf, ayf), textcoords='axes fraction',
                          va='center', color='k',
                          arrowprops=dict(arrowstyle='-', connectionstyle="arc3", ec='k', lw=1), zorder=100)
  
              if ni > n:
                  break
  
      _n = np.min([labels.shape[0], 20])
  
      annotate_obj(ax, number_of_annotations, labels[-1:-_n:-1], x[-1:-_n:-1], y[-1:-_n:-1], -1, -1, 'right')
      annotate_obj(ax, number_of_annotations, labels[:_n], x[:_n], y[:_n], 0, +1, 'left')
  
      if show_go_enrichment and ids is not None:
  
          # Calculate orders of magnitude (range)
          oomr = int(np.round(np.min(y))), int(np.round(np.max(y)))
  
          # First -1, last +1
          segments = [[x, x+1] for x in range(*oomr)]
          segments[0][0] -= 3
          segments[-1][1] += 3
  
          if not isinstance(colors, list):
              if not isinstance(colors, tuple):
                  colors = ('#1f77b4', '#d62728')
              # Build a continuous scale from low to high
              cmap = mpl.colors.LinearSegmentedColormap.from_list('custom', list(colors), len(segments))
              colors = [mpl.colors.rgb2hex(cmap(n)) for n in range(len(segments))]
  
          for n, (s, e) in enumerate(segments):
              if progress_callback:
                  progress_callback(float(n)/len(segments))
  
              mask = (y > s) & (y < e)
              c = x[mask]
  
              # Comparison relative to background
              gids = list(set(ids[mask]) - set(ids[~mask]))
              go = analysis.go_enrichment(gids, enrichment=go_enrichment, fdr=go_fdr)
  
              if go is not None:
                  labels = [gi[1] for gi in go.index]
  
                  # Filter out less specific GO terms where specific terms exist (simple text matching)
                  labels_f = []
                  for l in labels:
                      for ll in labels:
                          if l in ll and l != ll:
                              break
                      else:
                          labels_f.append(l)
                  labels = labels_f[:go_max_labels]
  
                  yr = ax.transLimits.transform( (0, y[c[0]]) )[1], ax.transLimits.transform( (0, y[c[-1]]) )[1]
  
                  # find axis label point for both start and end
                  if yr[0] < text_y_cross:
                      yr = yr[0]-slot_size, yr[1]
                  else:
                      yr = yr[0]+slot_size, yr[1]
  
                  yr = find_nearest_idx(text_y_slots, yr[0]), find_nearest_idx(text_y_slots, yr[1])
  
                  yrange = list(range(yr[0], yr[1]))
  
                  # Center ish
                  if len(yrange) > len(labels):
                      crop = (len(yrange) - len(labels)) // 2
                      if crop > 1:
                          yrange = yrange[crop:-crop]
  
                  # display ranked top to bottom
                  for idx, l in zip(yrange, labels):
                      axf = text_x_slots[idx]
                      ayf = text_y_slots[idx]
                      text_x_slots[idx] = np.nan
                      text_y_slots[idx] = np.nan
  
                      if ayf > text_y_cross:
                          ha = 'right'
                      else:
                          ha = 'left'
                      ax.text(axf, ayf, l, transform=ax.transAxes, ha=ha, color=colors[n])
  
              # Calculate GO enrichment terms for each region?
              ax.scatter(x[mask], y[mask], s=15, c=colors[n], lw=0, zorder=100)
  
      else:
              ax.scatter(x, y, s=15, c='k', lw=0, zorder=100)
  
      return ax