#! /usr/bin/env python3

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.widgets import Button, Cursor, SpanSelector, RectangleSelector, CheckButtons, RadioButtons
import lmfit
from datetime import datetime
import getpass
from pathlib import Path
from copy import deepcopy

from . import fit, misc, sim, figures, processing, anal
from .config import CM, COLORS, cprint

print = cprint


def select_traces(ppm_f1, ppm_f2, data, Neg=True):
    """
    Select traces from a 2D spectrum, save the coordinates in a list.
    Left click to select a point, right click to remove it.


    Parameters
    ----------
    ppm_f1 : 1darray
        ppm scale of the indirect dimension
    ppm_f2 : 1darray
        ppm scale of the direct dimension
    data : 2darray
        Spectrum
    Neg : bool
        Choose if to show the negative contours ( True) or not ( False )

    Returns
    -------
    coord : list
        List containing the ``[x,y]`` coordinates of the selected points.
    """
    cmaps = 'Blues_r', 'Reds_r'
    # Select traces from a 2D spectrum, save the coordinates in a list
    lvlstep = 1.4                  # for mouse scroll

    # Make the figure
    fig = plt.figure('Traces Selector')
    fig.set_size_inches(figures.figsize_large)
    ax = fig.add_subplot()
    ax.set_title('Left double click (or middle click) to add point, right click to remove point')
    plt.subplots_adjust(left=0.1, bottom=0.1, right=0.95, top=0.90)

    # Set figure borders
    xsx = max(ppm_f2)
    xdx = min(ppm_f2)
    ysx = max(ppm_f1)
    ydx = min(ppm_f1)

    # set level for contour
    livello = 0.2
    cnt = figures.ax2D(ax, ppm_f2, ppm_f1, data, xlims=(xsx, xdx), ylims=(ysx, ydx),
                       cmap=cmaps[0], c_fac=1.4, lvl=livello, lw=0.5, X_label='', Y_label='')
    if Neg:
        Ncnt = figures.ax2D(ax, ppm_f2, ppm_f1, -data, xlims=(xsx, xdx), ylims=(ysx, ydx),
                            cmap=cmaps[1], c_fac=1.4, lvl=livello, lw=0.5)
    else:
        Ncnt = None

    # Make pretty scales
    misc.pretty_scale(ax, (xsx, xdx), 'x')
    misc.pretty_scale(ax, (ysx, ydx), 'y')
    misc.set_fontsizes(ax, 20)

    xgrid = ppm_f2
    ygrid = ppm_f1

    # Parameters to save coordinates
    coord = []          # Final list of coordinates
    dot = []            # Bullets in figure
    dothline = []       # Horizontal lines
    dotvline = []       # Vertical lines

    def on_click(event):
        # What happens if you click?
        x, y = event.xdata, event.ydata     # x,y position of cursor
        if event.inaxes == ax:     # You are inside the figure
            ix, iy = misc.find_nearest(xgrid, x), misc.find_nearest(ygrid, y)       # Handle to the grid
            if (event.button == 1 and event.dblclick) or event.button == 2:     # Left click: add point
                if [ix, iy] not in coord:       # Avoid superimposed peaks
                    coord.append([ix, iy])       # Update list
                    # Update figure:
                    #   add bullet
                    line, = ax.plot(ix, iy, 'ro', markersize=2)
                    dot.append(line)
                    #   add horizontal line
                    dothline.append(ax.axhline(iy, c='r', lw=0.4))
                    #   add vertical line
                    dotvline.append(ax.axvline(ix, c='r', lw=0.4))
            if event.button == 3:     # Right click: remove point
                if [ix, iy] in coord:       # only if the point is already selected
                    # Remove coordinates and all figure elements
                    i = coord.index([ix, iy])
                    coord.remove([ix, iy])
                    killd = dot.pop(i)
                    killd.remove()
                    killh = dothline.pop(i)
                    killh.remove()
                    killv = dotvline.pop(i)
                    killv.remove()

        fig.canvas.draw()

    def on_scroll(event):
        # Zoom
        nonlocal livello, cnt
        if Neg:
            nonlocal Ncnt

        if event.button == 'up':
            livello *= lvlstep
        if event.button == 'down':
            livello /= lvlstep
        if livello > 1:
            livello = 1
        cnt, Ncnt = figures.redraw_contours(ax, ppm_f2, ppm_f1, data, lvl=livello,
                                            cnt=cnt, Neg=Neg, Ncnt=Ncnt, lw=0.5, cmap=cmaps)
        fig.canvas.draw()

    # Widgets
    cursor = Cursor(ax, useblit=True, color='k', linewidth=0.4)
    cursor.horizOn = True
    fig.canvas.mpl_connect('button_press_event', on_click)
    fig.canvas.mpl_connect('scroll_event', on_scroll)

    plt.show()
    plt.close()

    return coord


def select_for_integration(ppm_f1, ppm_f2, data, Neg=True):
    """
    Select the peaks of a 2D spectrum to integrate.
    First, select the area where your peak is located by dragging the red square.
    Then, select the center of the peak by right_clicking.
    Finally, click 'ADD' to store the peak. Repeat the procedure for as many peaks as you want.

    Parameters
    ----------
    ppm_f1 : 1darray
        ppm scale of the indirect dimension
    ppm_f2 : 1darray
        ppm scale of the direct dimension
    data : 2darray
        Spectrum
    Neg : bool
        Choose if to show the negative contours ( True) or not ( False )

    Returns
    -------
    peaks : list of dict
        For each peak there are two keys, 'f1' and 'f2', whose meaning is obvious.
        For each of these keys, you have 'u': center of the peak /ppm, and 'lim': the limits of the square you drew before.
    """

    cmaps = CM['Blues_r'], CM['Reds_r']
    lvlstep = 1.4                  # Increase step for contours when scroll the mouse1

    # Make an underlying grid to snap the pointer
    xgrid = np.copy(ppm_f2)
    ygrid = np.copy(ppm_f1)
    # Parameters to save coordinates
    coord = []          # Final list of coordinates
    rekt = []           # Rectangles
    # Set figure borders
    xsx, xdx = max(ppm_f2), min(ppm_f2)
    ysx, ydx = max(ppm_f1), min(ppm_f1)
    # set base level for contour
    lvl0 = 0.2

    # -----------------------------------------------------------------------------------------------------------------
    # Functions connected to the widgets
    def add_crosshair(coord, ix, iy):
        """ Add blue crosshair in (ix, iy) """
        if [ix, iy] not in coord:       # Avoid superimposed peaks
            coord.append([ix, iy])       # Update list
            ax.plot(ix, iy, 'bo', markersize=2)  # add dot
            ax.axhline(iy, c='b', lw=0.4)   # add horizontal line
            ax.axvline(ix, c='b', lw=0.4)   # add vertical line
            for obj in (tmp_dot, tmp_hline, tmp_vline):
                obj.set_visible(False)      # Set the red crosshair invisible
        return coord

    def on_click(event):
        """ Right click moves the red crosshair """
        x, y = event.xdata, event.ydata     # x,y position of cursor
        if event.inaxes == ax:   # You are inside the figure
            ix, iy = misc.find_nearest(xgrid, x), misc.find_nearest(ygrid, y)       # Snap to the grid
            if event.button == 3:
                # Update figure:
                tmp_dot.set_data((ix,), (iy,))
                tmp_hline.set_ydata((iy,))
                tmp_vline.set_xdata((ix,))
                # Make visible the red crosshair
                for obj in (tmp_dot, tmp_hline, tmp_vline):
                    obj.set_visible(True)
        else:
            pass
        fig.canvas.draw()

    def on_scroll(event):
        """ Redraw contours with more/less levels """
        nonlocal lvl0, cnt
        if Neg:
            nonlocal Ncnt

        # Read the input
        if event.button == 'up':
            lvl0 *= lvlstep
        if event.button == 'down':
            lvl0 /= lvlstep
        if lvl0 > 1:
            lvl0 = 1

        # Redraw contours
        if Neg:
            cnt, Ncnt = figures.redraw_contours(ax, ppm_f2, ppm_f1, data, lvl=lvl0, cnt=cnt, Neg=Neg, Ncnt=Ncnt, lw=0.5, cmap=[cmaps[0], cmaps[1]])
        else:
            cnt, _ = figures.redraw_contours(ax, ppm_f2, ppm_f1, data, lvl=lvl0, cnt=cnt, Neg=Neg, Ncnt=None, lw=0.5, cmap=[cmaps[0], cmaps[1]])
        # Draw the pretty things again
        misc.pretty_scale(ax, (xsx, xdx), 'x')
        misc.pretty_scale(ax, (ysx, ydx), 'y')
        misc.set_fontsizes(ax, 14)
        fig.canvas.draw()

    def onselect(epress, erelease):
        """ Drag rectangle """
        if epress.button == 1:   # left click
            # Vertices of the rectangle, counterclockwise
            X = np.array(span.extents[0:2])
            Y = np.array(span.extents[2:4])
            vertX = X[0], X[1], X[1], X[0]
            vertY = Y[0], Y[0], Y[1], Y[1]

            # Make visible the red rectangle
            if not tmp_rekt.get_visible():
                tmp_rekt.set_visible(True)
            tmp_rekt.set_xy(np.array((vertX, vertY)).T)  # .T because (vertX, vertY).shape = (2, 4)
        else:
            pass
        fig.canvas.draw()

    def add_func(event):
        """ ADD button """
        nonlocal coord
        # Draw blue crosshair reading data from the red dot
        ix, iy = tmp_dot.get_data()
        coord = add_crosshair(coord, ix, iy)    # Update coord with the new peak

        # Draw blue rectangle reading data from the red rectangle
        verts = np.array(tmp_rekt.get_xy())[:-1]    # Skip the latter because it knows it has to close the perimeter
        dummy_rekt, = ax.fill(verts[:, 0], verts[:, 1], 'tab:blue', alpha=0.25)
        rekt.append(dummy_rekt)
        # Set red rectangle to invisible
        tmp_rekt.set_visible(False)
        fig.canvas.draw()

    # -----------------------------------------------------------------------------------------------------------------

    # Make the figure
    fig = plt.figure('Manual Peak Picking')
    fig.set_size_inches(figures.figsize_large)
    ax = fig.add_subplot()
    ax.set_title('Drag with left peak for region; select peak with right click')
    plt.subplots_adjust(left=0.1, bottom=0.1, right=0.875, top=0.90)

    # ADD button
    add_box = plt.axes([0.925, 0.70, 0.05, 0.05])
    add_button = Button(add_box, 'ADD', hovercolor='0.975')

    # Draw contour
    cnt = figures.ax2D(ax, ppm_f2, ppm_f1, data, cmap=cmaps[0], c_fac=1.4, lvl=lvl0, lw=0.5)
    if Neg:
        Ncnt = figures.ax2D(ax, ppm_f2, ppm_f1, -data, cmap=cmaps[1], c_fac=1.4, lvl=lvl0, lw=0.5)

    # Initialize the red curves
    tmp_rekt, = ax.fill(np.array([0.1, 0.2, 0.3]), np.array([0.1, 0.2, 0.3]), 'tab:red', alpha=0.25, visible=False)  # Rectangle
    tmp_dot, = ax.plot(0, 0, 'ro', markersize=2, visible=False)  # Dot
    tmp_hline = ax.axhline(0, 0, c='r', lw=0.4, visible=False)  # Horizontal line
    tmp_vline = ax.axvline(0, 0, c='r', lw=0.4, visible=False)  # Vertical line

    # Pretty things
    misc.pretty_scale(ax, (xsx, xdx), 'x')
    misc.pretty_scale(ax, (ysx, ydx), 'y')
    misc.set_fontsizes(ax, 14)

    # Widgets
    cursor = Cursor(ax, useblit=True, color='red', linewidth=0.4)       # Moving crosshair
    cursor.horizOn = True
    fig.canvas.mpl_connect('button_press_event', on_click)      # Right click
    fig.canvas.mpl_connect('scroll_event', on_scroll)          # Mouse scroll
    span = RectangleSelector(ax, onselect, useblit=False, props=dict(facecolor='tab:red', alpha=0.5))    # Draggable rectangle
    add_button.on_clicked(add_func)  # Button

    plt.show()
    plt.close()

    # -----------------------------------------------------------------------------------------------------------------

    # collect results
    peaks = []

    def calc_borders(rect):
        """ Calculate the limits of the rectangle """
        vert = rect.get_xy()
        vertX, vertY = vert[:, 0], vert[:, 1]
        x_lims = min(vertX), max(vertX)
        y_lims = min(vertY), max(vertY)
        return x_lims, y_lims

    for dot, rec in zip(coord, rekt):
        x_lims, y_lims = calc_borders(rec)
        # Create an entry for each peak as stated in the description
        peaks.append({
            'f1': {
                'u': dot[1],
                'lim': y_lims,
                },
            'f2': {
                'u': dot[0],
                'lim': x_lims,
                },
            })
    return peaks


def noise_std(y):
    r"""
    Calculates the standard deviation of the noise using the Bruker formula:

    Taken :math:`y` as an array of :math:`N` points, and :math:`y[i]` its i-th entry:

    .. math::

       \sigma_N = \frac{1}{\sqrt{r-1}} \sqrt{ \sum_{k=0}^{r-1} (y[k]^2) - \frac{1}{r}
       [ ( \sum_{k=0}^{r-1} y[k] )^2 + \frac{3}{r^2 -1}( \sum_{k=0}^{r / 2 - 1} (k+1) (y[r/ 2 + k] - y[r/ 2 - k -1 ] ) )^2 ] }


    Parameters
    ----------
    y : 1darray
        The spectral region you would like to use to calculate the standard deviation of the noise.

    Returns
    -------
    noisestd : float
        The standard deviation of the noise.
    """
    N = len(y)
    n = N//2
    # W
    W = np.sum(y)**2
    # Y
    Y = np.sum(y**2)
    # X
    if N % 2 == 0:
        X = np.sum([(k+1) * (y[n+k] - y[n-k-1]) for k in range(n)])
    else:
        X = np.sum([(k+1) * (y[n+k] - y[n-k-1]) for k in range(n)])
    noisestd = (N-1)**(-0.5) * np.sqrt(Y - 1/N * (W + 3 * X**2 / (N**2 - 1)))
    return noisestd


def snr(x, data, s_reg=None, n_reg=None, gui=False):
    """
    Computes the signal to noise ratio of a 1D spectrum as height of the signal over twice the noise standard deviation.
    If you are not sure about what to put into ``s_reg`` and ``n_reg``,
    use the interactive interface by setting ``gui=True``.
    The values will be suggested.

    Parameters
    ----------
    x : 1darray
        Scale of the spectrum to use. The values in ``n_reg`` are searched according to this scale
    data : 1darray
        The spectrum of which you want to compute the SNR
    s_reg : float or tuple
        If ``float``, uses this value as maximum signal.
        If ``tuple``, the maximum value within the ``s_reg`` region is used as maximum signal.
        In this case, the limits of such region must be given according to ``x``.
        If ``None``, it is selected as the maximum value in ``data``
    n_reg : list or tuple, optional
        If provided, contains the points that delimit the noise region. Otherwise, the whole spectrum is used.
    gui : bool
        Set it to ``True`` to ignore ``s_reg`` and ``n_reg``, and use the GUI for the interactive estimation.

    Returns
    -------
    snr : float
        The SNR of the spectrum


    .. seealso::

        :func:`klassez.anal.noise_std`
        :func:`klassez.anal.snr_gui`

    """
    # Computes the SNR of a 1D spectrum (or 2D projection).
    # n_reg is a list/tuple of 2 values that delimitates the noise region

    # Interactive evaluation
    if gui:
        snr = anal.snr_gui(x, data)
        return snr

    # Evaluate signal
    if s_reg is None:
        signal = np.max(data)
    elif isinstance(s_reg, float):
        signal = s_reg
    else:
        x_s, y_s = misc.trim_data(x, data, s_reg)
        signal = np.max(y_s)

    # Evaluate noise
    if n_reg is None:
        y = deepcopy(data)
    else:
        x_n, y = misc.trim_data(x, data, n_reg)
    stdn = anal.noise_std(y)

    # Compute SNR
    snr = signal / (2 * stdn)
    return snr


def snr_2D(x_f1, x_f2, data, s_reg=None, n_reg=None, gui=False):
    """
    Computes the signal to noise ratio of a 2D spectrum as the height of the signal
    divided by twice the standard deviation of the noise, for each dimension.
    If you are not sure about what to put into ``s_reg`` and ``n_reg``,
    use the interactive interface by setting ``gui=True``.
    The values will be suggested.

    Parameters
    ----------
    x_f1 : 1darray
        PPM scale of the spectrum for the indirect dimension
    x_f2 : 1darray
        PPM scale of the spectrum for the direct dimension
    data : 2darray
        Spectrum of which you want to compute the SNR
    s_reg : float or list of tuple
        If ``float``, uses this value as maximum signal.
        If ``None``, it is selected as the maximum value in ``data``
        Otherwise, you have to provide the limits to draw a rectangle on ``data`` as ``[(f2_sx, f2_dx), (f1_sx, f1_dx)]``;
        the maximum signal is the highest point within this region.
    n_reg : list or tuple
        Contains the coordinates at which to extract empty traces for the evaluation of the noise standard deviation.
        It should be provided as ``[v2, v1]``, meaning that the noise-only trace of the indirect dimension will be taken at ``v2`` on ``x_f2``,
        and the noise-only trace of the direct dimension will be taken at ``v1`` on ``x_f1``.
        Leaving this parameter an ``None`` will open the interactive panel, and thus ``s_reg`` will be ignored.
    gui : bool
        Set it to ``True`` to ignore ``s_reg`` and ``n_reg``, and use the GUI for the interactive estimation.

    Returns
    -------
    snr_f1 : float
        The SNR of the indirect dimension
    snr_f2 : float
        The SNR of the direct dimension

    .. seealso::

        :func:`klassez.anal.noise_std`
        :func:`klassez.anal.snr_gui_2D`
    """
    # Computes the SNR of a 2D spectrum.
    # n_reg is basically mandatory!
    if n_reg is None:
        gui = True

    if gui:
        # Call the GUI that sorts everything automatically
        snr_f1, snr_f2 = anal.snr_gui_2D(x_f1, x_f2, data.real)
    else:
        # Evaluate what there is in s_reg
        if s_reg is None:       # Maximum of the whole dataset
            signal = np.max(data)
        elif isinstance(s_reg, float):  # The passed value IS the maximum
            signal = s_reg
        else:
            # Cut the spectrum around data according to s_reg
            _, _, z_trim = misc.trim_data_2D(x_f2, x_f1, data, s_reg[0], s_reg[1])
            # Find for the maximum within the rectangle
            signal = np.max(z_trim)

        # Get the projections
        f2 = anal.get_trace(data, x_f2, x_f1, n_reg[1], column=False)
        f1 = anal.get_trace(data, x_f2, x_f1, n_reg[0], column=True)
        # Compute noise std
        stdn_f1 = anal.noise_std(f1)
        stdn_f2 = anal.noise_std(f2)
        # Evaluate snr
        snr_f1 = signal / (2 * stdn_f1)
        snr_f2 = signal / (2 * stdn_f2)

    return float(snr_f1), float(snr_f2)


def get_trace(data, ppm_f2, ppm_f1, a, b=None, column=True):
    """

    Takes as input a 2D dataset and the ppm scales of direct and indirect dimensions respectively.
    Calculates the projection on the given axis summing from ``a`` (ppm) to ``b`` (ppm).
    Default: indirect dimension projection (i.e. ``column=True``), change it to False for the direct dimension projection.


    Parameters
    ----------
    data : 2darray
        Spectrum of which to extract the projections
    ppm_f2 : 1darray
        ppm scale of the direct dimension
    ppm_f1 : 1darray
        ppm scale of the indirect dimension
    a : float
        The ppm value from which to start extracting the projection.
    b : float, optional
        If provided, the ppm value at which to stop extracting the projection. Otherwise, returns only the ``a`` trace.
    column : bool
        If True, extracts the F1 projection. If False, extracts the F2 projection.

    Returns
    -------
    y : 1darray
        Computed projection
    """
    if b is None:
        b = a

    if column:
        A = misc.ppmfind(ppm_f2, a)[0]
        B = misc.ppmfind(ppm_f2, b)[0]+1
        w = slice(min(A, B), max(A, B))
        y = np.sum(data[..., w], axis=1)
    else:
        A = misc.ppmfind(ppm_f1, a)[0]
        B = misc.ppmfind(ppm_f1, b)[0]+1
        w = slice(min(A, B), max(A, B))
        y = np.sum(data[w, ...], axis=0)
    return y


def integral_2D(ppm_f1, t_f1, SFO1, ppm_f2, t_f2, SFO2, u_1=None, fwhm_1=200, utol_1=0.5, u_2=None, fwhm_2=200, utol_2=0.5, plot_result=False):
    """
    Calculate the integral of a 2D peak. The idea is to extract the traces correspondent to the peak center and fit them with a gaussian function in each dimension.
    Then, once got the intensity of each of the two gaussians, multiply them together in order to obtain the 2D integral.
    This procedure should be equivalent to what CARA does.

    .. note ::

        In development!!!


    Parameters
    ----------
    ppm_f1 : 1darray
        PPM scale of the indirect dimension
    t_f1 : 1darray
        Trace of the indirect dimension, real part
    SFO1 : float
        Larmor frequency of the nucleus in the indirect dimension
    ppm_f2 : 1darray
        PPM scale of the direct dimension
    t_f2 : 1darray
        Trace of the direct dimension, real part
    SFO2 : float
        Larmor frequency of the nucleus in the direct dimension
    u_1 : float
        Chemical shift in F1 /ppm. Defaults to the center of the scale
    fwhm_1 : float
        Starting FWHM /Hz in the indirect dimension
    utol_1 : float
        Allowed tolerance for u_1 during the fit. (u_1-utol_1, u_1+utol_1)
    u_2 : float
        Chemical shift in F2 /ppm. Defaults to the center of the scale
    fwhm_2 : float
        Starting FWHM /Hz in the direct dimension
    utol_2 : float
        Allowed tolerance for u_2 during the fit. (u_2-utol_2, u_2+utol_2)
    plot_result : bool
        True to show how the program fitted the traces.

    Returns
    -------
    I_tot : float
        Computed integral.
    """

    def f2min(param, T, x, SFO1):
        """ Cost function """
        par = param.valuesdict()
        sigma = misc.freq2ppm(par['fwhm'], np.abs(SFO1)) / (2 * (2 * np.log(2))**0.5)     # Convert FWHM to ppm and then to std
        model = sim.f_gaussian(x, par['u'], sigma, A=par['I'])      # Compute gaussian
        par['I'] = fit.fit_int(T, model)                            # Calculate integral
        residual = par['I'] * model - T
        return residual

    def fitting(ppm, T, SFO1, u_0, fwhm_0, utol=0.5):
        """ Main function """
        param = lmfit.Parameters()
        param.add('u', value=u_0, min=u_0-utol, max=u_0+utol)
        param.add('fwhm', value=fwhm_0, min=0)
        param.add('I', value=1, vary=False)         # Do not vary as it is adjusted during the fit

        minner = lmfit.Minimizer(f2min, param, fcn_args=(T, ppm, SFO1))
        result = minner.minimize(method='leastsq', max_nfev=10000, xtol=1e-10, ftol=1e-10)
        popt = result.params.valuesdict()

        # Calculate the model, update the popt dictionary
        sigma = misc.freq2ppm(popt['fwhm'], np.abs(SFO1)) / (2 * (2 * np.log(2))**0.5)
        model_0 = sim.f_gaussian(ppm, popt['u'], sigma, A=popt['I'])
        popt['I'] = fit.fit_int(T, model_0)
        model_0 *= popt['I']

        return popt, model_0

    # Calculate u_0 if not given
    if u_1 is None:
        u_1 = np.mean(ppm_f1)
    if u_2 is None:
        u_2 = np.mean(ppm_f2)

    # Fit both traces using the function above
    popt_f2, fit_f2 = fitting(ppm_f2, t_f2, SFO2, u_2, fwhm_2, utol_2)
    popt_f1, fit_f1 = fitting(ppm_f1, t_f1, SFO1, u_1, fwhm_1, utol_1)

    if plot_result:  # Do the plot
        xlim = [(max(ppm_f2), min(ppm_f2)),
                (max(ppm_f1), min(ppm_f1))]

        # Make the figure
        fig = plt.figure('Computed Integrals')
        fig.set_size_inches(figures.figsize_large)
        plt.subplots_adjust(left=0.05, right=0.95, bottom=0.10, top=0.90, wspace=0.20)

        axes = [fig.add_subplot(1, 2, w+1) for w in range(2)]
        axes[0].set_title('FIT F2')
        axes[1].set_title('FIT F1')
        axes[0].plot(ppm_f2, t_f2, c='tab:blue', label='Trace F2')
        axes[0].plot(ppm_f2, fit_f2, c='tab:red', lw=0.9, label='Fit F2')
        axes[0].plot(ppm_f2, t_f2-fit_f2, c='green', lw=0.6, label='residual')
        axes[1].plot(ppm_f1, t_f1, c='tab:blue', label='Trace F1')
        axes[1].plot(ppm_f1, fit_f1, c='tab:red', lw=0.9, label='Fit F1')
        axes[1].plot(ppm_f1, t_f1-fit_f1, c='green', lw=0.6, label='residual')

        # Fancy shit
        for k, ax in enumerate(axes):
            misc.pretty_scale(ax, xlim[k], 'x')
            misc.pretty_scale(ax, ax.get_ylim(), 'y')
            misc.mathformat(ax)
            ax.set_xlabel(r'$\delta$ /ppm')
            ax.legend()
            misc.set_fontsizes(ax, 16)

        plt.show()
        plt.close()

    # Calculate integral
    I_tot = popt_f1['I'] * popt_f2['I']
    return I_tot


def integrate(ppm0, data0, SFO1, filename='integrals', X_label=r'$\delta\,$F1 /ppm', dx=1):
    r"""
    Allows interactive integration of a NMR spectrum through a dedicated GUI. Returns the values as a dictionary,
    where the keys are the selected regions truncated to the 3nd decimal figure.
    The values are saved in the ``<filename>.igrl`` file.

    In the GUI, draw the integration region around the peak.
    The shape of the integral function appears in red in that region, and the value of the integral
    is reported under the "current integral" label on the right side.
    The integral can be corrected with a baseline, that is the straight line that connects the border of
    the integration window. It can be activated and deactivated using the checkbox on the right side.
    Press "ADD" to save the integral value. Upon pressing, the integral function becomes green and the
    value appears on top of the figure.
    To remove an integral from the list, first click on the corresponding value on the top to select it: the number
    and the integral function should become purple. Then, click on the "REMOVE" button. Use the right click to reset
    the selection.

    At the end of the procedure, click on the "SAVE" button to write the `.igrl` file.

    Parameters
    ----------
    ppm : 1darray
        PPM scale of the spectrum
    data : 1darray
        Spectrum to be integrated.
    SFO1 : float
        Larmor frequency of the observed nucleus.
        For conversion to the correct integration scale.
    filename : str
        Name for the `.igrl` file to be written. Without extension!
    X_label : str
        Label of the x-axis
    dx : float
        Correction for the integral values. It should be ``dx = 2 * dw``

    Returns
    -------
    abs_vals : dict
        Dictionary containing the values of the integrated peaks.


    .. seealso ::

        :func:`klassez.anal.write_igrl`

    """

    # Copy to prevent overwriting
    ppm = np.copy(ppm0)
    data = np.copy(data0)

    # Create the frequency scale to use for integration. The offset is not important
    x = misc.ppm2freq(ppm, SFO1)

    # Calculate the total integral function
    int_f = processing.integral(data, x) * dx

    # Make the figure
    fig = plt.figure('Spectrum Integration')
    fig.set_size_inches(figures.figsize_large)
    plt.subplots_adjust(left=0.05, bottom=0.10, top=0.90, right=0.88)
    ax = fig.add_subplot()

    # Transform for text coordinates for integral: x is in ax reference frame, y is in fig reference frame
    mixed_transform = mpl.transforms.blended_transform_factory(ax.transData, fig.transFigure)

    # Declare variables
    abs_vals = {}               # dictionary: integrals of the peaks, absolute values
    text_integrals = {}         # dictionary: text that appears at the top
    perm_integrals = {}         # dictionary: green functions
    sel_key = None              # selected integral to be removed with the REMOVE button

    # Make boxes for widgets
    add_box = plt.axes([0.915, 0.80, 0.05, 0.06])
    remove_box = plt.axes([0.915, 0.72, 0.05, 0.06])
    save_box = plt.axes([0.915, 0.20, 0.05, 0.06])
    check_box = plt.axes([0.89, 0.35, 0.10, 0.06])
    # Make buttons
    add_button = Button(add_box, 'ADD', hovercolor='0.875')
    save_button = Button(save_box, 'SAVE', hovercolor='0.875')
    remove_button = Button(remove_box, 'REMOVE', hovercolor='0.875')

    # Make the checkbox
    checkbox = CheckButtons(check_box, ['Use baseline'], actives=[False])
    #   Adjust the position and the dimension of the tick box
    misc.edit_checkboxes(checkbox, xadj=0, yadj=0.05, dim=100, color='violet')

    # ---------------------------------------------------------------------------------------
    # Functions connected to the widgets

    def check_clicked(label):
        """ Slot for the checkbox """
        # Get the region of the red curve
        x_values = tmp_plot.get_xdata()
        # Draw it again with/without baseline and correct the value
        onselect(x_values[0], x_values[-1])

    def calc_bas(isx, idx):
        """ Compute straight line baseline """
        # Two points
        bas_points = np.array([data[isx], data[idx]])
        # Use the linear regression to connect them, employ the point scale
        _, (bas_m, bas_q) = fit.lr(bas_points, x=np.asarray([isx, idx]))
        # full-length x-scale for baseline
        xb = np.arange(isx, idx+1, 1)
        # baseline = mx + q
        bas = xb * bas_m + bas_q
        return bas

    def onselect(vsx, vdx):
        """ When you drag and release """
        # Take indexes of the borders of the selected window and sort them
        isx = misc.ppmfind(ppm, vsx)[0]
        idx = misc.ppmfind(ppm, vdx)[0]
        isx, idx = min(isx, idx), max(isx, idx)
        # Make a slice
        sl = slice(isx, idx+1)
        # If you select less than three points you are not integrating anything
        if idx - isx < 3:
            return

        # Compute the baseline according to the checkbox status
        if checkbox.get_status()[0]:        # do
            bas = calc_bas(isx, idx)
            # and make it visible
            bas_plot.set_visible(True)
        else:                               # don't
            bas = np.zeros_like(ppm[sl])
            # and make it disappear
            bas_plot.set_visible(False)
        # Update baseline plot
        bas_plot.set_data(ppm[sl], bas)

        # Compute the integral
        int_fun = processing.integral(data[sl] - bas, x[sl])    # Integral function ( primitive )
        int_val = (int_fun[-1] - int_fun[0]) * dx               # Value of the integral

        # Update the plot
        #   compute correction factor to make the curve actually visible
        yscale = (max(data[sl]) - min(data[sl])) / (max(int_fun) - min(int_fun))
        #   Plot the red curve
        tmp_plot.set_data(ppm[sl], yscale * int_fun)
        #   and set it visible
        tmp_plot.set_visible(True)
        #   update text under the red label
        tmp_text.set_text(f'{int_val:12.5g}')
        fig.canvas.draw()

    def f_add(event):
        """ When you click 'ADD' """
        # Set the integral function as invisible so that it does not overlay with the permanent one
        tmp_plot.set_visible(False)
        # Get the data from the red curve
        xdata, ydata = tmp_plot.get_data()
        # Get the number from under the red label
        int_val = eval(tmp_text.get_text())
        # Key comes from xdata, value comes from the red label
        abs_vals['{:.3f}:{:.3f}'.format(xdata[0], xdata[-1])] = int_val

        # Update the plot
        #   draw the green curve in place of the red one and store the artist in a dictionary
        perm_integrals['{:.3f}:{:.3f}'.format(xdata[0], xdata[-1])] = ax.plot(xdata, ydata, c='tab:green', lw=2.2)[-1]
        #   draw the text at the top
        text_integrals['{:.3f}:{:.3f}'.format(xdata[0], xdata[-1])] = ax.text(
                np.mean(xdata), 0.90, '{:12.5g}'.format(int_val),       # in the center of the window, at the top panel
                ha='center', va='bottom', transform=mixed_transform, fontsize=10, rotation=60)
        fig.canvas.draw()

    def update_selkey(event):
        """ For the selection. There are a ton of early returns! """
        nonlocal sel_key
        # Right click resets the selection
        if event.button == 3:
            sel_key = None
            return

        # MouseButtonEvent gives the position in pixels -> conversion needed
        figw_px, figh_px = fig.get_figwidth() * fig.dpi, fig.get_figheight() * fig.dpi
        if event.y / figh_px < 0.9:     # y coordinate below the top border of the subplot
            return

        # You made until here. Nice!

        # Placeholders for the selection
        keys, locs = [], []
        # Loop over all the texts
        for key, text_obj in text_integrals.items():
            # find the center position of the text in fig frame
            bbox_display = text_obj.get_window_extent(fig.canvas.get_renderer())
            bbox_fig = bbox_display.transformed(fig.transFigure.inverted())
            # Save the keys of the dictionary and the positions in the lists
            keys.append(key)
            locs.append((bbox_fig.x0 + bbox_fig.x1) / 2)

        # Mouse click position in fig frame
        relp = event.x / figw_px
        # Find the closest text from where you clicked
        nearest = misc.find_nearest(locs, relp)
        # You need to click sufficiently close!
        if abs(relp - nearest) > 0.005:
            return
        # Selection worked! Find the index and put the selection
        i_nearest = locs.index(nearest)
        sel_key = keys[i_nearest]

    def on_mouse_click(event):
        """ Click to select a certain integral """
        # DO NOT GO FURTHER if there are not integrals
        if len(text_integrals.keys()) == 0:
            return
        # Select the integral
        update_selkey(event)

        if sel_key is not None:
            # Highlight in blue
            text_integrals[sel_key].set_color('indigo')
            perm_integrals[sel_key].set_color('indigo')
        else:
            # Reset: text in black, function in green
            for key in text_integrals.keys():
                text_integrals[key].set_color('k')
                perm_integrals[key].set_color('tab:green')
        fig.canvas.draw()

    def f_remove(event):
        """ Remove the selected integral """
        nonlocal sel_key
        # DO NOT GO FURTHER if an integral is not selected
        if sel_key is None:
            return
        # Kill the artists
        text_integrals[sel_key].remove()
        perm_integrals[sel_key].remove()
        # Remove the things from the dictionaries
        abs_vals.pop(sel_key)
        text_integrals.pop(sel_key)
        perm_integrals.pop(sel_key)
        # Update the plot
        fig.canvas.draw()
        # Reset the selection
        sel_key = None

    def f_save(event):
        """ When you click 'SAVE' """
        # Do NOT close the panel if there were not integrals computed
        if len(abs_vals.keys()) == 0:
            print('No integrals taken yet!')
            return
        # Write the .igrl file and close
        anal.write_igrl(filename, abs_vals, indirect_scale=None, header=True)
        plt.close()
        print(f'Integrals saved in {filename}.igrl.')

    # ---------------------------------------------------------------------------------------

    # Add things to the figure panel

    ax.plot(ppm, data, c='tab:blue', lw=0.8)        # Spectrum
    # Draw the total integral function but set to invisible because it is useless, needed as placeholder for the red curve
    tmp_plot, = ax.plot(ppm, int_f/max(int_f)*max(data), c='tab:red', lw=2.2, visible=False)
    bas_plot, = ax.plot(ppm, np.zeros_like(data), c='violet', lw=1.8, visible=False)

    # Draw text labels in the figure, on the right
    ax.text(0.94, 0.68, 'Current integral', ha='center', va='center', transform=fig.transFigure, fontsize=14, color='tab:red')
    tmp_text = ax.text(0.94, 0.65, '0', horizontalalignment='center', verticalalignment='center', transform=fig.transFigure, fontsize=14)

    # Fancy shit
    ax.set_xlim(max(ppm), min(ppm))
    ax.set_xlabel(X_label)
    ax.set_ylabel('Intensity /a.u.')
    misc.pretty_scale(ax, ax.get_xlim(), 'x')
    misc.pretty_scale(ax, ax.get_ylim(), 'y')
    misc.mathformat(ax, 'y')
    misc.set_fontsizes(ax, 14)

    # Add more widgets and connect the buttons to their functions
    add_button.on_clicked(f_add)
    remove_button.on_clicked(f_remove)
    save_button.on_clicked(f_save)
    checkbox.on_clicked(check_clicked)
    fig.canvas.mpl_connect('button_press_event', on_mouse_click)
    # Here I need to keep a reference to it otherwise the span selector and the cursor do not show
    cursor = Cursor(ax, c='tab:red', lw=0.8, horizOn=False)
    span = SpanSelector(ax, onselect, 'horizontal', props=dict(facecolor='tab:red', alpha=0.5))
    # Useless but used to avoid flake to become angry
    span.set_visible(True)
    cursor.vertOn = True

    # Show the figure
    plt.show()

    return abs_vals


def integrate_p2D(ppm0, data0, SFO1, ref=0, indirect_scale=None, filename='integrals', X_label=r'$\delta\,$F1 /ppm', dx=1):
    r"""
    Allows interactive integration of a (series of) NMR spectrum through a dedicated GUI. Returns the values as a dictionary,
    where the keys are the selected regions truncated to the 3nd decimal figure.
    The values are saved in the ``<filename>.igrl`` file.

    In the GUI, draw the integration region around the peak.
    The shape of the integral function appears in red in that region, and the value of the integral
    is reported under the "current integral" label on the right side.
    Both are referred to the ``ref``-th row of ``data``.
    The trend of the integrals appear in the inset plot at the bottom left corner.
    The integral can be corrected with a baseline, that is the straight line that connects the border of
    the integration window. It can be activated and deactivated using the checkbox on the right side.
    Press "ADD" to save the integral values. Upon pressing, the integral function becomes green and the
    value appears on top of the figure.
    To remove an integral from the list, first click on the corresponding value on the top to select it: the number
    and the integral function should become purple. Then, click on the "REMOVE" button. Use the right click to reset
    the selection.

    At the end of the procedure, click on the "SAVE" button to write the `.igrl` file.

    Parameters
    ----------
    ppm : 1darray
        PPM scale of the spectrum
    data : 1darray
        Spectrum to be integrated.
    SFO1 : float
        Larmor frequency of the observed nucleus.
        For conversion to the correct integration scale.
    ref : int
        Index of the transient to be used as reference when drawing the functions
    indirect_scale : 1darray
        Scale of the indirect dimension, if any. If ``None``, 1, 2, 3 ... are used.
    filename : str
        Name for the `.igrl` file to be written. Without extension!
    X_label : str
        Label of the x-axis
    dx : float
        Correction for the integral values. It should be ``dx = 2 * dw``

    Returns
    -------
    abs_vals : dict
        Dictionary containing the values of the integrated peaks.


    .. seealso ::

        :func:`klassez.anal.write_igrl`

    """

    # Copy to prevent overwriting
    ppm = np.copy(ppm0)
    all_data = np.copy(data0)
    data = np.copy(all_data[ref])

    # Create the frequency scale to use for integration. The offset is not important
    x = misc.ppm2freq(ppm, SFO1)
    if indirect_scale is None:
        indirect_scale = np.arange(all_data.shape[0]) + 1
    labels = [f'{label:>12.5g}' for label in indirect_scale]

    # Calculate the total integral function
    int_f = processing.integral(data, x)

    # Make the figure
    fig = plt.figure('Spectrum Integration')
    fig.set_size_inches(figures.figsize_large)
    plt.subplots_adjust(left=0.205, bottom=0.10, top=0.90, right=0.88)
    ax = fig.add_subplot()
    axy = fig.add_axes([0.020, 0.10, 0.15, 0.25])

    # Transform for text coordinates for integral: x is in ax reference frame, y is in fig reference frame
    mixed_transform = mpl.transforms.blended_transform_factory(ax.transData, fig.transFigure)

    # Declare variables
    abs_vals = {}               # dictionary: integrals of the peaks, absolute values
    text_integrals = {}         # dictionary: text that appears at the top
    perm_integrals = {}         # dictionary: green functions
    sel_key = None              # selected integral to be removed with the REMOVE button

    # Make boxes for widgets
    add_box = plt.axes([0.915, 0.80, 0.05, 0.06])
    remove_box = plt.axes([0.915, 0.72, 0.05, 0.06])
    save_box = plt.axes([0.915, 0.20, 0.05, 0.06])
    check_box = plt.axes([0.89, 0.35, 0.10, 0.06])
    # Make buttons
    add_button = Button(add_box, 'ADD', hovercolor='0.875')
    save_button = Button(save_box, 'SAVE', hovercolor='0.875')
    remove_button = Button(remove_box, 'REMOVE', hovercolor='0.875')

    # Make the checkbox
    checkbox = CheckButtons(check_box, ['Use baseline'], actives=[False])
    #   Adjust the position and the dimension of the tick box
    misc.edit_checkboxes(checkbox, xadj=0, yadj=0.05, dim=100, color='violet')

    # ---------------------------------------------------------------------------------------
    # Functions connected to the widgets

    def check_clicked(label):
        """ Slot for the checkbox """
        # Get the region of the red curve
        x_values = tmp_plot.get_xdata()
        # Draw it again with/without baseline and correct the value
        onselect(x_values[0], x_values[-1])

    def calc_bas(y, isx, idx):
        """ Compute straight line baseline """
        # Two points
        bas_points = np.array([y[isx], y[idx]])
        # Use the linear regression to connect them, employ the point scale
        _, (bas_m, bas_q) = fit.lr(bas_points, x=np.asarray([isx, idx]))
        # full-length x-scale for baseline
        xb = np.arange(isx, idx+1, 1)
        # baseline = mx + q
        bas = xb * bas_m + bas_q
        return bas

    def calc_all_bas(isx, idx):
        """ Compute the straight line baseline for all the experiments """
        all_bas = np.array([
            calc_bas(y, isx, idx)
            for y in all_data])
        return all_bas

    def axy_plot(y):
        """ Draw a series in the miniplot """
        # Set the data and make it visible
        int_plot.set_ydata(y)
        int_plot.set_visible(True)
        # Update the limits
        misc.set_ylim(axy, y)
        fig.canvas.draw()

    def axy_reset():
        """ Turn off the miniplot """
        # Draw zeros and make it invisible
        int_plot.set_ydata(np.zeros_like(indirect_scale))
        int_plot.set_visible(False)
        # Update the limits
        misc.set_ylim(axy, np.zeros_like(indirect_scale))
        fig.canvas.draw()

    def onselect(vsx, vdx):
        """ When you drag and release """
        # Take indexes of the borders of the selected window and sort them
        isx = misc.ppmfind(ppm, vsx)[0]
        idx = misc.ppmfind(ppm, vdx)[0]
        isx, idx = min(isx, idx), max(isx, idx)
        # Make a slice
        sl = slice(isx, idx+1)
        # If you select less than three points you are not integrating anything
        if idx - isx < 3:
            return

        # Compute the baseline according to the checkbox status
        if checkbox.get_status()[0]:        # do
            bas = calc_bas(data, isx, idx)
            # and make it visible
            bas_plot.set_visible(True)
        else:                               # don't
            bas = np.zeros_like(ppm[sl])
            # and make it disappear
            bas_plot.set_visible(False)
        # Update baseline plot
        bas_plot.set_data(ppm[sl], bas)

        # Compute the integral
        int_fun = processing.integral(data[sl] - bas, x[sl])    # Integral function ( primitive )
        int_val = (int_fun[-1] - int_fun[0]) * dx               # Value of the integral

        # Compute all integrals
        if checkbox.get_status()[0]:        # do
            all_bas = calc_all_bas(isx, idx)
        else:                               # don't
            all_bas = np.zeros_like(all_data[..., sl])
        all_int = processing.integrate(all_data[..., sl] - all_bas, x[sl], dx=dx)

        # Update the plot
        #   compute correction factor to make the curve actually visible
        yscale = (max(data[sl]) - min(data[sl])) / (max(int_fun) - min(int_fun))
        #   Plot the red curve
        tmp_plot.set_data(ppm[sl], yscale * int_fun)
        #   and set it visible
        tmp_plot.set_visible(True)
        #   update text under the red label
        tmp_text.set_text(f'{int_val:12.5g}')

        # Update the minipanel
        axy_plot(all_int)
        fig.canvas.draw()

    def f_add(event):
        """ When you click 'ADD' """
        # Set the integral function as invisible so that it does not overlay with the permanent one
        tmp_plot.set_visible(False)
        # Get the data from the red curve
        xdata, ydata = tmp_plot.get_data()
        int_val = eval(tmp_text.get_text())

        # Compute all the integrals of the series
        #   Get the window limits from the red curve
        isx = misc.ppmfind(ppm, min(xdata))[0]
        idx = misc.ppmfind(ppm, max(xdata))[0]
        isx, idx = min(isx, idx), max(isx, idx)
        #   Make a slice
        sl = slice(isx, idx+1)

        #   Baseline
        if checkbox.get_status()[0]:        # do
            all_bas = calc_all_bas(isx, idx)
        else:                               # don't
            all_bas = np.zeros_like(all_data[..., sl])
        # Compute the integrals
        all_int = dx * processing.integrate(all_data[..., sl] - all_bas, x[sl])

        # Key comes from xdata, value comes from the red label
        abs_vals['{:.3f}:{:.3f}'.format(xdata[0], xdata[-1])] = all_int

        # Update the plot
        #   draw the green curve in place of the red one and store the artist in a dictionary
        perm_integrals['{:.3f}:{:.3f}'.format(xdata[0], xdata[-1])] = ax.plot(xdata, ydata, c='tab:green', lw=2.2)[-1]
        #   draw the text at the top
        text_integrals['{:.3f}:{:.3f}'.format(xdata[0], xdata[-1])] = ax.text(
                np.mean(xdata), 0.90, '{:12.5g}'.format(int_val),       # in the center of the window, at the top panel
                ha='center', va='bottom', transform=mixed_transform, fontsize=10, rotation=60)
        #   make the miniplot invisible
        axy_reset()
        fig.canvas.draw()

    def update_selkey(event):
        """ For the selection. There are a ton of early returns! """
        nonlocal sel_key
        # Right click resets the selection
        if event.button == 3:
            sel_key = None
            return

        # MouseButtonEvent gives the position in pixels -> conversion needed
        figw_px, figh_px = fig.get_figwidth() * fig.dpi, fig.get_figheight() * fig.dpi
        if event.y / figh_px < 0.9:     # y coordinate below the top border of the subplot
            return

        # You made until here. Nice!

        # Placeholders for the selection
        keys, locs = [], []
        # Loop over all the texts
        for key, text_obj in text_integrals.items():
            # find the center position of the text in fig frame
            bbox_display = text_obj.get_window_extent(fig.canvas.get_renderer())
            bbox_fig = bbox_display.transformed(fig.transFigure.inverted())
            # Save the keys of the dictionary and the positions in the lists
            keys.append(key)
            locs.append((bbox_fig.x0 + bbox_fig.x1) / 2)

        # Mouse click position in fig frame
        relp = event.x / figw_px
        # Find the closest text from where you clicked
        nearest = misc.find_nearest(locs, relp)
        # You need to click sufficiently close!
        if abs(relp - nearest) > 0.005:
            return
        # Selection worked! Find the index and put the selection
        i_nearest = locs.index(nearest)
        sel_key = keys[i_nearest]

    def on_mouse_click(event):
        """ Click to select a certain integral """
        # DO NOT GO FURTHER if there are not integrals
        if len(text_integrals.keys()) == 0:
            return
        # Select the integral
        update_selkey(event)

        if sel_key is not None:
            # Highlight in blue
            text_integrals[sel_key].set_color('indigo')
            perm_integrals[sel_key].set_color('indigo')
            axy_plot(abs_vals[sel_key])
        else:
            # Reset: text in black, function in green
            for key in text_integrals.keys():
                text_integrals[key].set_color('k')
                perm_integrals[key].set_color('tab:green')
        fig.canvas.draw()

    def f_remove(event):
        """ Remove the selected integral """
        nonlocal sel_key
        # DO NOT GO FURTHER if an integral is not selected
        if sel_key is None:
            return
        # Kill the artists
        text_integrals[sel_key].remove()
        perm_integrals[sel_key].remove()
        axy_reset()
        # Remove the things from the dictionaries
        abs_vals.pop(sel_key)
        text_integrals.pop(sel_key)
        perm_integrals.pop(sel_key)
        # Update the plot
        fig.canvas.draw()
        # Reset the selection
        sel_key = None

    def f_save(event):
        """ When you click 'SAVE' """
        # Do NOT close the panel if there were not integrals computed
        if len(abs_vals.keys()) == 0:
            print('No integrals taken yet!')
            return
        # Write the .igrl file and close
        anal.write_igrl(filename, abs_vals, indirect_scale=indirect_scale, header=True)
        plt.close()
        print(f'Integrals saved in {filename}.igrl.')

    # ---------------------------------------------------------------------------------------

    # Add things to the figure panel

    for k, (y, c, label) in enumerate(zip(all_data, COLORS, labels)):
        ax.plot(ppm, y, c=c, lw=0.8, label=label)        # Spectrum
    # Draw the total integral function but set to invisible because it is useless, needed as placeholder for the red curve
    tmp_plot, = ax.plot(ppm, int_f/max(int_f)*max(data), c='tab:red', lw=2.2, visible=False)
    bas_plot, = ax.plot(ppm, np.zeros_like(data), c='violet', lw=1.8, visible=False)
    int_plot, = axy.plot(indirect_scale, np.zeros_like(indirect_scale), '.-', ms=5, c='tab:red', visible=False)

    # Draw text labels in the figure, on the right
    ax.text(0.94, 0.68, 'Current integral', ha='center', va='center', transform=fig.transFigure, fontsize=14, color='tab:red')
    tmp_text = ax.text(0.94, 0.65, '0', horizontalalignment='center', verticalalignment='center', transform=fig.transFigure, fontsize=14)

    # Fancy shit
    ax.set_xlim(max(ppm), min(ppm))
    ax.set_xlabel(X_label)
    ax.set_ylabel('Intensity /a.u.')
    misc.pretty_scale(ax, ax.get_xlim(), 'x')
    misc.pretty_scale(ax, ax.get_ylim(), 'y')
    misc.mathformat(ax, 'y')
    misc.set_fontsizes(ax, 14)
    ax.legend(loc='upper left', bbox_to_anchor=(0.0125, 0.975), bbox_transform=fig.transFigure, fontsize=9, ncols=2)
    axy.set_xlabel('Indirect dimension')
    misc.pretty_scale(axy, axy.get_xlim(), 'x', 4)
    misc.pretty_scale(axy, axy.get_ylim(), 'y', 4)
    misc.mathformat(axy, 'y')
    misc.set_fontsizes(axy, 14)

    # Add more widgets and connect the buttons to their functions
    add_button.on_clicked(f_add)
    remove_button.on_clicked(f_remove)
    save_button.on_clicked(f_save)
    checkbox.on_clicked(check_clicked)
    fig.canvas.mpl_connect('button_press_event', on_mouse_click)
    # Here I need to keep a reference to it otherwise the span selector and the cursor do not show
    cursor = Cursor(ax, c='tab:red', lw=0.8, horizOn=False)
    span = SpanSelector(ax, onselect, 'horizontal', props=dict(facecolor='tab:red', alpha=0.5))
    # Useless but used to avoid flake to become angry
    span.set_visible(True)
    cursor.vertOn = True

    # Show the figure
    plt.show()

    return abs_vals


def integrate_2D(ppm_f1, ppm_f2, data, SFO1, SFO2, fwhm_1=200, fwhm_2=200, utol_1=0.5, utol_2=0.5, plot_result=False):
    """
    Function to select and integrate 2D peaks of a spectrum, using dedicated GUIs.
    Calls integral_2D to do the dirty job.

    .. error::

        Old function!! Legacy


    Parameters
    ----------
    ppm_f1 : 1darray
        PPM scale of the indirect dimension
    ppm_f2 : 1darray
        PPM scale of the direct dimension
    data : 2darray
        real part of the spectrum
    SFO1 : float
        Larmor frequency of the nucleus in the indirect dimension
    SFO2 : float
        Larmor frequency of the nucleus in the direct dimension
    fwhm_1 : float
        Starting FWHM /Hz in the indirect dimension
    fwhm_2 : float
        Starting FWHM /Hz in the direct dimension
    utol_1 : float
        Allowed tolerance for u_1 during the fit. (u_1-utol_1, u_1+utol_1)
    utol_2 : float
        Allowed tolerance for u_2 during the fit. (u_2-utol_2, u_2+utol_2)
    plot_result : bool
        True to show how the program fitted the traces.

    Returns
    -------
    I : dict
        Computed integrals. The keys are ``'<ppm f1>:<ppm f2>'`` with 2 decimal figures.
    """

    # Get all the information that integral_2D needs
    peaks = anal.select_for_integration(ppm_f1, ppm_f2, data, Neg=True)

    Int = {}      # Declare empty dictionary
    for P in peaks:
        # Extract trace F1
        T1 = anal.get_trace(data, ppm_f2, ppm_f1, P['f2']['u'], column=True)
        x_T1, y_T1 = misc.trim_data(ppm_f1, T1, *P['f1']['lim'])    # Trim according to the rectangle
        # Extract trace F2
        T2 = anal.get_trace(data, ppm_f2, ppm_f1, P['f1']['u'], column=False)
        x_T2, y_T2 = misc.trim_data(ppm_f2, T2, *P['f2']['lim'])    # Trim according to the rectangle

        # Compute the integrals
        I_p = processing.integral_2D(x_T1, y_T1, SFO1, x_T2, y_T2, SFO2,
                                     u_1=P['f1']['u'], fwhm_1=fwhm_1, utol_1=utol_1,
                                     u_2=P['f2']['u'], fwhm_2=fwhm_2, utol_2=utol_2,
                                     plot_result=plot_result)

        # Store the integral in the dictionary
        Int[f'{P["f2"]["u"]:.2f}:{P["f1"]["u"]:.2f}'] = I_p
    return Int


def write_igrl(filename, dic, indirect_scale=None, header=False):
    """
    Write a section in a integral report file, which shows the integrated regions and the values of the peaks. It allows

    Parameters
    ----------
    filename : str
        Path to the file to be written
    dic : dict
        Dictionary of integral values. The keys are 'ppm1:ppm2' and the
        associated values are the integrals, as floats or as sequences.
    indirect_scale : 1darray
        Scale of the indirect dimension. To be used in the future for fitting
    header : bool
        If True, adds a "!" starting line to separate fit trials

    Returns
    -------
    None

    .. seealso::

        :func:`klassez.anal.read_igrl`

        :func:`klassez.anal.integrate`

    """
    filename = Path(filename).with_suffix('.igrl')
    # Understand how many integrals per window we have to write
    for key, value in dic.items():
        if isinstance(value, (int, float)):
            # Integrals of only one spectrum
            n_spectra = 1
        else:
            # Pseudo 2D
            n_spectra = len(value)
        # Information achieved, exit from this useless loop
        break

    # Header line of the table
    spectra_idx = [f'I {w+1}' for w in range(n_spectra)]
    firstline = '; '.join([
        f'{"#":4s}', f'{"Window /ppm":>18s}', *[f'{w:>12s}' for w in spectra_idx]
        ])

    # 4 [#] + 18 [Window] + 12*n_spectra [I] + 2*n_spectra [; ] + 2 [first ;] + 1 [extra]
    n_dashes = 25 + 14 * n_spectra

    # Open the file
    f = filename.open('a', buffering=1)

    # Info on the region to be fitted
    if header:
        now = datetime.now()
        date_and_time = now.strftime("%d/%m/%Y at %H:%M:%S")
        f.write('! Integrals computed by {} on {}\n\n'.format(getpass.getuser(), date_and_time))

    # Write the indirect scale
    if indirect_scale is not None and n_spectra > 1:
        f.write('x = [')
        for x in indirect_scale:
            f.write(f'{x:.5g}, ')
        f.write(']\n\n')

    # Start writing the table
    f.write(firstline+'\n')
    f.write('-' * n_dashes + '\n')

    # Write the lines of the table
    for k, (key, value) in enumerate(dic.items()):
        # First two entries: number of the window and ppm extremes
        things_to_write = [f'{k+1:4.0f}', f'{key:>18s}']
        # Fork if value is a number or a sequence
        if isinstance(value, (int, float)):     # number
            things_to_write.append(f'{value:12.5e}')
        else:                                   # sequence
            things_to_write.extend([f'{x:12.5e}' for x in value])
        # Make a single line to write
        line = '; '.join(things_to_write)
        # Write it
        f.write(line + '\n')
    # \bottomrule
    f.write('-' * n_dashes + '\n\n')

    # End the section
    f.write('=' * 96 + '\n\n')
    # Close the file
    f.close()


def read_igrl(filename, n=-1):
    """
    Reads a `.igrl` file, containing the integrals of either one or a series of spectra.
    The file is separated and unpacked into a dictionary, each of which contains the integration windows with three decimal figures as keys
    and the integrals as their associated value.
    If present, the indirect timescale is also read and returned.

    Parameters
    ----------
    filename : str
        Path to the filename to be read
    n : int
        Number of performed integrating procedure to be read. Default: last one. The breakpoints are lines that start with "!".
        For this reason, ``n=0`` returns an empty dictionary, hence the first attempt is ``n=1``.

    Returns
    -------
    dic : dict
        Integrals
    indirect_scale : 1darray or None
        If there is a line that starts with ``'x = '``, it is interpreted as the scale for the indirect dimension.
        If there is not, ``None`` is returned.


    .. seealso::

        :func:`klassez.fit.write_igrl`

    """
    def read_region(R):
        """ Creates a dictionary of parameters from a section of the input file.  """
        # Placeholders
        indirect_scale = None
        dic_r = {}
        # Separate the lines and remove the empty ones
        R = R.split('\n')
        for k, r in enumerate(R):
            if len(r) == 0 or r.isspace():
                _ = R.pop(k)

        n_bp = 0        # Number of breaking points (----)
        for k, r in enumerate(R):
            if '------' in r:   # Increase breakpoint and store the line number
                n_bp += 1
                continue

            if n_bp == 0:
                # Before the table, seek for the indirect timescale
                if 'x = ' in r:
                    indirect_scale = np.array(eval(r.split('=')[-1]))

            if n_bp == 1:       # Second section: integrals
                line = r.split(';')  # Separate the values
                # Get the key from the second column
                key = line[1].replace(' ', '')
                # Unpack the rest of the line
                values = [eval(w) for w in line[2:]]
                # Substitute a float if there is only one value, otherwise convert to array
                if len(values) > 1:
                    values = np.asarray(values)
                else:
                    values = values[-1]

                # Put the values in the dictionary
                dic_r[key] = values

            if n_bp == 2:   # End of file: stop reading
                break

        return dic_r, indirect_scale

    # Read the file
    ff = Path(filename).read_text()
    # Get the actual section from an output file
    f = ff.split('!')[n]
    # Separate the bigger sections
    R = f.split('='*96)
    # Remove the empty lines
    for k, r in enumerate(R):
        if r.isspace():
            _ = R.pop(k)

    # Get the values. R has only one entry!
    dic, indirect_scale = read_region(R[0])
    return dic, indirect_scale


def snr_gui(x, y):
    """
    GUI for the evaluation of the Signal to Noise Ratio of a 1D spectrum.

    Select "Signal" on the top right corner and drag a region to highlight the
    reference signal approximate position. The detected point appears as a blue X.

    Then, select "Noise". Drag a signal-free region, i.e. where there is only noise.
    This will be used for the estimation of the noise standard deviation.
    The noise level will be highlighted in the figure by two red dashed lines.
    If these lines do not visually match the noise level, it is most likely there
    is a signal included in the noise region.

    When both the signal and the noise are present, the SNR will be computed.
    The selection can be refined as many times as one wants, until the figure panel is closed.
    Close the figure to return the values, and to print the used ``s_reg`` and ``n_reg`` to be given
    to :func:`klassez.anal.snr`.

    Parameters
    ----------
    x : 1darray
        PPM scale of the spectrum
    y : 1darray
        Spectrum

    Returns
    -------
    snr : float
        Estimated signal to noise ratio.
    """
    # Variables placeholder
    s_reg = None
    n_reg = None

    # Make the figure
    fig = plt.figure('Evaluate SNR')
    fig.set_size_inches(15, 8)
    plt.subplots_adjust(top=0.95, bottom=0.1, left=0.10, right=0.8)
    ax = fig.add_subplot()

    # Make boxes for widgets
    radio_box = plt.axes([0.825, 0.80, 0.15, 0.15])

    box_expl = plt.axes([0.825, 0.10, 0.15, 0.30])
    box_expl.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    box_expl.spines.left.set_visible(False)
    box_expl.spines.right.set_visible(False)
    # Make the widgets
    radio_labels = ['Signal', 'Noise']
    radio = RadioButtons(radio_box, labels=radio_labels, active=0, activecolor='k',
                         label_props=dict(color=['tab:blue', 'tab:red'], fontsize=[14, 14]))
    # Text placeholders
    s_reg_text = ax.text(0.90, 0.75, 'None', ha='center', va='center',
                         transform=fig.transFigure, color='tab:blue', fontsize=14)
    s_text = ax.text(0.90, 0.70, 'Signal: None', ha='center', va='center',
                     transform=fig.transFigure, color='tab:blue', fontsize=14)
    n_reg_text = ax.text(0.90, 0.65, 'None', ha='center', va='center',
                         transform=fig.transFigure, color='tab:red', fontsize=14)
    n_text = ax.text(0.90, 0.60, r'$\sigma_N$: None', ha='center', va='center',
                     transform=fig.transFigure, color='tab:red', fontsize=14)
    ax.text(0.90, 0.50, 'Signal-to-Noise Ratio', ha='center', va='center',
            transform=fig.transFigure, color='k', fontsize=14)
    snr_text = ax.text(0.90, 0.47, 'None', ha='center', va='center',
                       transform=fig.transFigure, color='k', fontsize=18)

    # tutorial
    box_expl.text(0.01, 0.5, '\n'.join([
        'Select "Signal" to drag the signal region (blue).',
        'The blue X marks the detected maximum signal.',
        '',
        'Select "Noise" to drag the noise region (blue).',
        'The red horizontal lines mark the estimated noise level.',
        '',
        'The signal to noise ratio is evaluated when both are present.',
        ]), ha='left', va='center', transform=box_expl.transAxes,
                  fontsize=10, color='k', wrap=True)

    # SLOTS
    def radio_selection(label):
        """ Activate the correct span selector """
        trigg = int(radio.index_selected)
        if trigg == 0:  # Signal
            span_n.set_active(False)
            span_s.set_active(True)
        else:           # Noise
            span_s.set_active(False)
            span_n.set_active(True)
        fig.canvas.draw()

    def compute_signal(s_reg):
        """ Detect the signal position and height in ``s_reg`` """
        x_t, y_t = misc.trim_data(x, y, s_reg)
        k_rel = np.argmax(y_t)
        xm = x_t[k_rel]
        ym = y_t[k_rel]
        return xm, ym

    def compute_noisestd(n_reg):
        """ Compute the noise standard deviation in ``n_reg`` """
        x_t, y_t = misc.trim_data(x, y, n_reg)
        noise_std = anal.noise_std(y_t)
        mean_yn = np.mean(y_t)
        return noise_std, mean_yn

    def on_select(vsx, vdx):
        """ When dragging the slider """
        # Get the selected area of whatever slider it is
        selected = sorted([vsx, vdx], reverse=True)

        if int(radio.index_selected) == 0:      # Signal
            nonlocal s_reg
            # Write s_reg in the permanent variable and on the plot
            s_reg = selected
            s_reg_text.set_text(' : '.join([f'{w:8.3f}' for w in s_reg]))
            # Get the max signal value and position
            xm, ym = compute_signal(s_reg)
            # Write the value
            s_text.set_text(misc.expformat(ym))
            # Draw the cross
            cross.set_data([xm], [ym])
            cross.set_visible(True)
        elif int(radio.index_selected) == 1:    # Noise
            if selected[0] - selected[1] < 5 * misc.calcres(x):
                return
            nonlocal n_reg
            # Write n_reg in the permanent variable and on the plot
            n_reg = selected
            n_reg_text.set_text(' : '.join([f'{w:8.3f}' for w in n_reg]))
            # Compute the noise standard deviation and write it in the plot
            noise_std, mean_yn = compute_noisestd(n_reg)
            n_text.set_text(misc.expformat(noise_std))
            # Draw the red lines as mean_yn +/- noise_std
            for k, line in enumerate([upper_line, lower_line]):
                line.set_ydata([mean_yn + (-1)**k * noise_std])
                line.set_visible(True)
        # Try to compute the SNR
        eval_snr(ret=False)
        fig.canvas.draw()

    def eval_snr(ret=False):
        """ Compute SNR based on ``s_reg`` and ``n_reg`` """
        # DO NOTHING if one of the two is missing
        if s_reg is None or n_reg is None:
            return None
        # Compute the SNR
        _, signal = compute_signal(s_reg)
        noise_std, _ = compute_noisestd(n_reg)
        snr = signal / (2 * noise_std)
        if ret:     # Return the value
            return snr
        else:       # Write the value on the figure
            snr_text.set_text(f'{snr:.2f}')

    # Draw the spectrum
    figures.ax1D(ax, x, y, c='k', X_label=r'$\delta\,$ /ppm')
    # Placeholders, set invisible because with nothing selected they are useless
    #   Blue cross
    cross, = ax.plot([np.mean(x)], [y[len(y)//2]], 'x', c='b', ms=10, visible=False)
    #   Red lines
    upper_line = ax.axhline(1, lw=0.7, ls='--', c='r', visible=False)
    lower_line = ax.axhline(-1, lw=0.7, ls='--', c='r', visible=False)

    # Cosmetic stuff
    misc.set_fontsizes(ax, 16)

    # Connect widgets to slots
    span_s = SpanSelector(ax, on_select, direction='horizontal', interactive=True,
                          drag_from_anywhere=True, snap_values=x,
                          props=dict(facecolor='tab:blue', alpha=0.2))
    span_s.set_active(True)    # Only the signal selector must start active
    span_n = SpanSelector(ax, on_select, direction='horizontal', interactive=True,
                          drag_from_anywhere=True, snap_values=x,
                          props=dict(facecolor='tab:red', alpha=0.2))
    span_n.set_active(False)    # Only the signal selector must start active
    radio.on_clicked(radio_selection)

    # Start event loop
    plt.show()
    plt.close()

    # If something went wrong and the SNR is not computed, early return
    if s_reg is None or n_reg is None:
        return

    # Compute SNR on the basis of the current s_reg and n_reg
    snr = eval_snr(ret=True)
    # Print the obtained value
    print(f'SNR = {snr:.2f}', c='violet')
    print('Selected regions for SNR:', c='violet')
    fmt_s_reg = f'({max(s_reg):.3f}, {min(s_reg):.3f})'
    fmt_n_reg = f'({max(n_reg):.3f}, {min(n_reg):.3f})'
    print(f's_reg={fmt_s_reg}, n_reg={fmt_n_reg}')
    print()

    return snr


def snr_gui_2D(x_f1, x_f2, yy):
    """
    GUI for the evaluation of the Signal to Noise Ratio of a 2D spectrum.

    Select "Signal" on the top right corner and drag a rectangle to highlight the
    reference signal height. The detected point appears as a blue X.

    Then, select "Noise". A red cross-cursor will appear. Find a position where
    you can extract a signal-free region, i.e. where there is only noise.
    Double click with the left button of the mouse to extract the projection in that point:
    they will appear as red traces. These will be used for the estimation of the noise standard
    deviation.

    When both the signal and the noise are present, the SNR will be computed.
    The selection can be refined as many times as one wants, until the figure panel is closed.
    Close the figure to return the values, and to print the used ``s_reg`` and ``n_reg`` to be given
    to :func:`klassez.anal.snr_2D`.

    Parameters
    ----------
    x_f1 : 1darray
        PPM scale of the spectrum for the indirect dimension
    x_f2 : 1darray
        PPM scale of the spectrum for the direct dimension
    yy : 2darray
        Spectrum

    Returns
    -------
    snr_f1 : float
        Estimated signal to noise ratio in the indirect dimension.
    snr_f2 : float
        Estimated signal to noise ratio in the direct dimension.
    """
    # Variables placeholder
    s_reg = None
    n_reg = None

    # for the plot
    lvl = 0.2       # Starting level
    lvlstep = 1.4   # Increase factor
    #   for the scaling of the red traces
    swp2 = misc.get_extent(x_f2)
    swp1 = misc.get_extent(x_f1)

    # Make the figure
    fig = plt.figure('Evaluate SNR')
    fig.set_size_inches(15, 8)
    plt.subplots_adjust(top=0.95, bottom=0.1, left=0.10, right=0.8)
    ax = fig.add_subplot()

    # Make boxes for widgets
    #   radiobuttons signal/noise
    radio_box = plt.axes([0.825, 0.80, 0.15, 0.15])
    #   Explanation
    box_expl = plt.axes([0.825, 0.10, 0.15, 0.30])
    #   Deactivate the ticks and the lateral spines
    box_expl.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
    box_expl.spines.left.set_visible(False)
    box_expl.spines.right.set_visible(False)

    # Make the widgets
    radio_labels = ['Signal', 'Noise']
    radio = RadioButtons(radio_box, labels=radio_labels, active=0, activecolor='k',
                         label_props=dict(color=['tab:blue', 'tab:red'], fontsize=[14, 14]))
    # Text placeholders
    s_reg_text = ax.text(0.90, 0.75, 'None', ha='center', va='center',
                         transform=fig.transFigure, color='tab:blue', fontsize=14)
    s_text = ax.text(0.90, 0.70, 'Signal: None', ha='center', va='center',
                     transform=fig.transFigure, color='tab:blue', fontsize=14)
    n_reg_text = ax.text(0.90, 0.65, 'None', ha='center', va='center',
                         transform=fig.transFigure, color='tab:red', fontsize=14)
    n_text = ax.text(0.90, 0.60, r'$\sigma_N$ F$_2$: None, $\sigma_N$ F$_1$: None',
                     ha='center', va='center', transform=fig.transFigure,
                     color='tab:red', fontsize=14)
    ax.text(0.90, 0.52, 'Signal-to-Noise Ratio', ha='center', va='center',
            transform=fig.transFigure, color='k', fontsize=14)
    snr_text = ax.text(0.90, 0.47, 'None\n', ha='center', va='center',
                       transform=fig.transFigure, color='k', fontsize=18)

    # tutorial
    box_expl.text(0.01, 0.5, '\n'.join([
        'Select "Signal" to drag the signal region (blue).',
        'The blue X marks the detected maximum signal.',
        '',
        'Select "Noise" to select the traces for the estimation of the noise level (red).',
        'Double click to evaluate the projections at the cursor position.',
        'The extracted projections appear as red lines on the figure where they were taken.',
        'Right click to hide the plots.',
        ]), ha='left', va='center', transform=box_expl.transAxes,
                  fontsize=10, color='k', wrap=True)

    # SLOTS
    def radio_selection(label):
        """ Activate the span selector or the cursor alternatively """
        trigg = int(radio.index_selected)
        if trigg == 0:  # Signal
            span_n.set_active(False)
            span_s.set_active(True)
        else:           # Noise
            span_s.set_active(False)
            span_n.set_active(True)
        fig.canvas.draw()

    def compute_signal(s_reg):
        """ Detect the signal position and height in ``s_reg`` """
        # Cut the rectangle according to s_reg
        x_t, y_t, z_t = misc.trim_data_2D(x_f2, x_f1, yy, s_reg[0], s_reg[1])
        # Where is the maximum -> array of array hence [-1] to unpack
        k_f1, k_f2 = np.argwhere(z_t == np.max(z_t))[-1]
        xm = x_t[k_f2]          # coordinate on x_f2
        ym = y_t[k_f1]          # coordinate on x_f1
        zm = z_t[k_f1, k_f2]    # actual maximum value
        return xm, ym, zm

    def compute_noisestd(v_2, v_1, draw=False):
        """
        Compute the noise standard deviation in ``n_reg``.
        Extract trace in F1 @ ``v_2`` and trace in F2 @ ``v_1``.
        ``draw == True`` updates the figure.
        """
        trf1 = anal.get_trace(yy, x_f2, x_f1, v_2, column=True)
        trf2 = anal.get_trace(yy, x_f2, x_f1, v_1, column=False)

        if draw:    # Update the red traces
            draw_traces(v_2, v_1, trf1, trf2)

        # Compute the std of the noise
        noisestd_f1 = anal.noise_std(trf1)
        noisestd_f2 = anal.noise_std(trf2)
        return noisestd_f1, noisestd_f2

    def on_select(click_event, release_event):
        """ When dragging the blue rectangle"""
        # Get the selected area
        x1 = click_event.xdata
        y1 = click_event.ydata
        x2 = release_event.xdata
        y2 = release_event.ydata
        # Avoid selections less than 3x3 points in whatever dimension
        if abs(x2 - x1) < 3 * misc.calcres(x_f2):
            return
        if abs(y2 - y1) < 3 * misc.calcres(x_f1):
            return

        # Sort them
        sel_x = sorted([x1, x2], reverse=True)
        sel_y = sorted([y1, y2], reverse=True)

        if int(radio.index_selected) == 0:      # Signal
            nonlocal s_reg
            # Write s_reg in the permanent variable and on the plot
            s_reg = [sel_x, sel_y]
            s_reg_text.set_text('\n'.join([
                ' : '.join([f'{w:8.3f}' for w in tmp])
                for tmp in s_reg]))
            # Get the max signal value and position
            xm, ym, zm = compute_signal(s_reg)
            # Write the value
            s_text.set_text(misc.expformat(zm))
            # Draw the cross
            cross.set_data([xm], [ym])
            cross.set_visible(True)
        # Try to compute the SNR
        eval_snr(ret=False)
        fig.canvas.draw()

    def on_click(event):
        """ For the cursor """
        def is_in(scale, value):
            """ Check if value is within scale extents """
            return min(scale) < value and value < max(scale)

        # EARLY RETURNS if something is not specific
        if radio.index_selected == 0:   # case 1: signal is selected
            return
        if event.inaxes != ax:      # case 2: click outside the panel
            return
        if event.button == 3:       # case 3: right click
            # Disable the red traces and stop
            line_f2.set_visible(False)
            line_f1.set_visible(False)
            fig.canvas.draw()
            return
        elif not (event.button == 1 and event.dblclick):    # case whatever!
            return

        # Basically, I survive until here ONLY if I double-clicked with the left
        # mouse button inside the plot panel

        # EARLY RETURNS to make sure that you can get a projection
        if is_in(x_f2, event.xdata):
            v_2 = event.xdata
        else:
            return
        if is_in(x_f1, event.ydata):
            v_1 = event.ydata
        else:
            return

        # Basically, I survive until here ONLY if:
        # 1. double-clicked with left mouse button inside the figure
        # 2. the position where I clicked is both inside the ppm scale of
        #    F1 and F2

        # Write n_reg in the permanent location and update the text
        nonlocal n_reg
        n_reg = [v_2, v_1]
        n_reg_text.set_text(f'F$_2$ @ {v_1:8.3f}, F$_1$ @ {v_2:8.3f}')
        # Compute noise STD
        noisestd_f1, noisestd_f2 = compute_noisestd(v_2, v_1, draw=True)
        # Write them in the figure
        text_f2 = r'$\sigma_N$ F$_2$: ' + f'{misc.expformat(noisestd_f2)}'
        text_f1 = r'$\sigma_N$ F$_1$: ' + f'{misc.expformat(noisestd_f1)}'
        n_text.set_text('\n'.join([text_f2, text_f1]))

        # Try to evaluate the SNR
        eval_snr(ret=False)
        fig.canvas.draw()

    def draw_traces(pos_f1, pos_f2, trf1, trf2):
        """ Draw the red traces """
        # extent of the traces = max - min
        ext_tf1 = misc.get_extent(trf1)
        ext_tf2 = misc.get_extent(trf2)
        # final height: 1/15 of the other dimension
        h1 = swp2 / 15
        h2 = swp1 / 15

        # The traces must be normalized to their extent,
        # multiplied for their height to make them visible,
        # then shifted to the position they were taken from
        line_f2.set_ydata(trf2 / ext_tf2 * h2 + pos_f2)
        line_f1.set_xdata(trf1 / ext_tf1 * h1 + pos_f1)
        # Make them visible
        line_f2.set_visible(True)
        line_f1.set_visible(True)

    def eval_snr(ret=False):
        """ Compute SNR based on ``s_reg`` and ``n_reg`` """
        # DO NOTHING if one of the two is missing
        if s_reg is None or n_reg is None:
            return None
        # Compute the SNR
        _, _, signal = compute_signal(s_reg)
        noise_std_f1, noise_std_f2 = compute_noisestd(*n_reg)
        snr_f1 = signal / (2 * noise_std_f1)
        snr_f2 = signal / (2 * noise_std_f2)
        if ret:     # Return the values
            return snr_f1, snr_f2
        else:       # Write the values on the figure
            snr_text.set_text('\n'.join([
                f'F$_1$: {snr_f1:.2f}',
                f'F$_2$: {snr_f2:.2f}',
                ]))

    # HERE BELOW slots for the scroll
    def increase_zoom(event):
        """ step of the scroll """
        nonlocal lvlstep
        lvlstep += 0.05

    def decrease_zoom(event):
        """ step of the scroll + safety check """
        nonlocal lvlstep
        lvlstep -= 0.05
        if lvlstep < 1.05:
            lvlstep = 1.05

    def on_scroll(event):
        """ What happens when you scroll """
        nonlocal lvl, cnt
        if event.button == 'up':
            lvl *= lvlstep
        if event.button == 'down':
            lvl /= lvlstep
        if lvl > 1:
            lvl = 1
        # Redraw the contours
        cnt, _ = figures.redraw_contours(ax, x_f2, x_f1, yy, lvl=lvl, cnt=cnt, cmap=['Greys_r', None])
        fig.canvas.draw()

    # Draw the spectrum
    cnt = figures.ax2D(ax, x_f2, x_f1, yy, cmap='Greys_r', lvl=lvl, X_label=r'$\delta\,$ F2 /ppm', Y_label=r'$\delta\,$ F1 /ppm')
    # Placeholders, set invisible because with nothing selected they are useless
    #   Blue cross
    cross, = ax.plot([np.mean(x_f2)], [np.mean(x_f1)], 'x', c='b', ms=10, visible=False, zorder=11)
    #   Red lines   -> zorder to make them always be on top
    line_f2, = ax.plot(x_f2, np.ones_like(x_f2)*np.mean(x_f1), lw=0.7, ls='-', c='r', visible=False, zorder=10)
    line_f1, = ax.plot(np.ones_like(x_f1)*np.mean(x_f2), x_f1, lw=0.7, ls='-', c='r', visible=False, zorder=10)

    # Cosmetic stuff
    misc.set_fontsizes(ax, 16)

    # Connect widgets to slots
    #   Radiobuttons
    radio.on_clicked(radio_selection)
    #   Rectangle
    span_s = RectangleSelector(ax, on_select, interactive=True, drag_from_anywhere=True,
                               # I want the handle to exist but to be invisible
                               handle_props=dict(markersize=0, alpha=0, visible=False),
                               props=dict(facecolor='tab:blue', edgecolor='tab:blue', alpha=0.2))
    span_s.set_active(True)    # Only the signal selector must start active
    #   Cursor
    span_n = Cursor(ax, horizOn=True, vertOn=True, lw=1.5, color='tab:red')
    span_n.set_active(False)    # Only the signal selector must start active
    #   Clicks
    fig.canvas.mpl_connect('button_press_event', on_click)
    #   Scroll
    fig.canvas.mpl_connect('scroll_event', on_scroll)

    # Start event loop
    plt.show()
    plt.close()

    # If something went wrong and the SNR is not computed, early return
    if s_reg is None or n_reg is None:
        return

    # Compute SNR on the basis of the current s_reg and n_reg
    snr_f1, snr_f2 = eval_snr(ret=True)
    # Print the obtained value
    print(f'SNR F1 = {snr_f1:.2f}, SNR F2 = {snr_f2:.2f}', c='violet')
    print('Selected regions for SNR:', c='violet')
    fmt_s_reg = f'[({max(s_reg[0]):.3f}, {min(s_reg[0]):.3f}), ' +\
                f'({max(s_reg[1]):.3f}, {min(s_reg[1]):.3f})]'
    fmt_n_reg = f'({n_reg[0]:.3f}, {n_reg[1]:.3f})'
    print(f's_reg={fmt_s_reg}, n_reg={fmt_n_reg}')
    print()

    return snr_f1, snr_f2
