"""Module containing the plotting classes. The methods correspond to different types of plots to create"""

try:
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    import pandas as pd
    from natsort import natsorted
    from bokeh.plotting import figure, output_file, show
    from bokeh.models import Whisker, BasicTicker, ColorBar, LinearColorMapper, PrintfTickFormatter
    import colorcet as cc
    import bokeh as bk
    import math
except ModuleNotFoundError:
    raise ModuleNotFoundError('Some dependencies might be missing. Check installation and try again')
except Exception as err:
    print('Unexpected Error')
    print(err)
else:
    if __name__ == '__main__':
        print("Modules have been loaded")


class Plot:
    """
    Plot objects Master Class from which the rest inherit

    :param stack: Value to denote if barplots should stack
    :type stack: Bool
    :param value: Data to be plotted. Can be 'isotopologue_fraction', 'corrected area' or 'mean_enrichment'
    :type value: str
    :param data: IsoplotData object containing clean data
    :type data: Pandas Dataframe
    :param name: Name for generated file directory where plots will go
    :type name: str
    :param metabolite: metabolite to be plotted
    :type metabolite: str
    :param condition: List of conditions to be plotted
    :type condition: list
    :param time: List of times to be plotted
    :type time: list
    """

    WIDTH = 1080
    HEIGHT = 640

    def __init__(self, stack, value, data, name, metabolite, condition, time):

        self.stack = stack
        self.value = value
        self.data = data
        self.name = name
        self.metabolite = metabolite
        self.condition = condition
        self.time = time
        self.filtered_data = self.data[
            (self.data['metabolite'] == self.metabolite) &
            (self.data['condition'].isin(self.condition)) &
            (self.data['time'].isin(self.time))]

    @staticmethod
    def split_ids(ids):
        """
        Function to split IDs and get back lists of conditions, times and replicates in order

        :param ids: IDs generated by IsoplotData Object
        :type ids: list
        :return: lists containing conditions, times and replicates
        :rtype: lists
        """

        count = ids[0].count('_')

        if count == 2:

            conditions, times, replicates = [], [], []

            for i in ids:
                c, t, r = i.split("_")
                conditions.append(c)
                times.append(t)
                replicates.append(r)

            return conditions, times, replicates

        if count == 1:

            conditions, times = [], []

            for i in ids:
                c, t = i.split("_")
                conditions.append(c)
                times.append(t)

            return conditions, times

        else:

            raise ValueError('Number of underscores is different from 2 or 3')


class StaticPlot(Plot):
    """
    Class to generate the different static plots.

    :param fmt: Output format of static plots (pdf, svg, png or jpeg)
    :type fmt: str
    :param display: Should plots be displayed when created
    :type display: Bool
    """

    def __init__(self, stack, value, data, name, metabolite,
                 condition, time, fmt, display=False):
        """

        """

        super().__init__(stack, value, data, name, metabolite, condition, time)
        self.fmt = fmt
        self.static_fig_name = self.metabolite + '.' + self.fmt
        self.display = display

    def stacked_areaplot(self):
        """Creation of area stackplot (for cinetic data)"""

        # Commençons par la préparation de data
        stackpivot = self.filtered_data.pivot(
            index='ID', columns='isotopologue', values=self.value)
        stackpivot = stackpivot.reindex(index=natsorted(stackpivot.index))
        pivotcol = stackpivot[0:].to_numpy()
        stackyval = pivotcol.transpose()
        stackxval = stackpivot.index.to_numpy()

        # Passons au plot
        plt.figure(figsize=[38, 20])
        labels = list(stackpivot.columns)
        plt.stackplot(stackxval,
                      stackyval,
                      labels=labels,
                      colors=cc.glasbey_dark[:len(
                          self.data['isotopologue'].unique())])
        plt.legend()
        plt.title("{} CID cinetics".format(self.metabolite))
        plt.xticks(rotation=45)
        plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
        plt.tight_layout()
        plt.savefig(self.static_fig_name, bbox_inches='tight', format=self.fmt)

        if self.display:
            plt.show()
        else:
            plt.close()

    def barplot(self):
        """Creation of barplots"""

        # Nous filtrons et préparons la table pour les donnéees
        mydata = self.filtered_data
        mydatapivot = mydata.pivot_table(index=["condition_order", 'ID'],
                                         columns='isotopologue',
                                         values=self.value)

        # Ici nous mettons les données dans l'ordre choisi par le template
        mydatapivot.sort_index(level="condition_order", inplace=True)
        mydatapivot = mydatapivot.droplevel(level="condition_order")

        # Passons au plot
        sns.set_context("poster")
        ax = mydatapivot.plot.bar(stacked=self.stack,
                                  figsize=(30, 15),
                                  title=self.metabolite,
                                  color=cc.glasbey_dark[:len(
                                      self.data['isotopologue'].unique())])
        ax.set_xlabel('Condition, Time and Replicate')
        ax.set_ylabel(self.value)
        ax.set_xticklabels(ax.get_xticklabels(),
                           rotation=45,
                           horizontalalignment='right')
        ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
        plt.tight_layout()
        plt.savefig(self.static_fig_name, bbox_inches='tight', format=self.fmt)
        if self.display:
            plt.show()
        else:
            plt.close()

    def mean_barplot(self):
        """Creation of meaned barplots (on replicates)"""

        # Ici nous faisons les moyennes et les SD des données et nous les mettons dans un df
        df_replicate_mean = self.filtered_data.groupby(
            ["condition_order", "condition", "time", "isotopologue"])[self.value].mean()
        df_replicate_std = self.filtered_data.groupby(
            ["condition_order", "condition", "time", "isotopologue"])[self.value].std()
        df_full = pd.concat([df_replicate_mean, df_replicate_std], axis=1)
        df_full.columns = ("mean", "std")

        # Nous formattons le df pour pouvoir plotter
        df_ready = df_full.unstack()
        df_ready.sort_index(level="condition_order", inplace=True)
        df_ready = df_ready.droplevel(level="condition_order")

        # Passons au plot
        sns.set_context("poster")
        colors = cc.glasbey_dark[:len(df_ready.columns)]
        this_ax = df_ready["mean"].plot.bar(stacked=self.stack,
                                            yerr=df_ready['std'],
                                            figsize=(30, 15),
                                            title=self.metabolite,
                                            color=colors)
        this_ax.set_xlabel('Condition, Time and Replicate')
        this_ax.set_ylabel(self.value)
        this_ax.set_xticklabels(this_ax.get_xticklabels(),
                                rotation=45,
                                horizontalalignment='right')
        this_ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
        plt.xticks(rotation=45)
        plt.tight_layout()
        plt.savefig(self.static_fig_name, bbox_inches='tight', format=self.fmt)
        if self.display:
            plt.show()
        else:
            plt.close()

    def mean_enrichment_plot(self):
        """Generate static mean_enrichment plots"""

        # Nous préparons une liste et un df dans lesquels on va ajouter les mean_enrichment
        list_of_tmpdfs = []

        # Nous retirons les duplicats pour chaque ID individuel
        for ID in self.filtered_data["ID"].drop_duplicates():
            tmpdf = self.filtered_data[self.filtered_data["ID"] == ID].drop_duplicates(
                subset=['mean_enrichment'])
            list_of_tmpdfs.append(tmpdf)

        # Nous mettons l'ordre de conditions en fonction du template, et nous préparons les datas pour plotter
        mean_enrichment_df = pd.concat(list_of_tmpdfs, ignore_index=True)
        mean_enrichment_df = mean_enrichment_df[["ID", "condition_order", "mean_enrichment"]]
        mean_enrichment_df.sort_values(by="condition_order", inplace=True)
        mean_enrichment_df.drop(labels="condition_order", axis=1, inplace=True)
        mean_enrichment_df.set_index("ID", inplace=True)

        # Nous plottons les data avec la fonction de pandas
        sns.set_context("poster")
        ax = mean_enrichment_df.plot.bar(figsize=(30, 15),
                                         title=self.metabolite,
                                         color=cc.glasbey_dark[3])
        ax.set_xlabel('Condition, Time and Replicate')
        ax.set_ylabel("mean_enrichment")
        ax.set_xticklabels(ax.get_xticklabels(), rotation=45, horizontalalignment='right')
        ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
        plt.tight_layout()
        plt.savefig(self.static_fig_name, bbox_inches='tight', format=self.fmt)
        if self.display:
            plt.show()
        else:
            plt.close()

    def mean_enrichment_meanplot(self):
        """Generate static mean_enrichment plots with meaned replicates"""

        # Nous préparons une liste et un df dans lesquels on va ajouter les mean_enrichment
        list_of_tmpdfs = []

        # Nous retirons les duplicats pour chaque ID individuel
        for ID in self.filtered_data["ID"].drop_duplicates():
            tmpdf = self.filtered_data[
                self.filtered_data["ID"] == ID].drop_duplicates(subset=['mean_enrichment'])
            list_of_tmpdfs.append(tmpdf)
        mean_enrichment_df = pd.concat(list_of_tmpdfs, ignore_index=True)

        # Ici nous faisons les moyennes et les SD des données et nous les mettons dans un df
        df_replicate_mean = mean_enrichment_df.groupby(
            ["condition_order", "condition", "time", "isotopologue"])['mean_enrichment'].mean()
        df_replicate_std = mean_enrichment_df.groupby(
            ["condition_order", "condition", "time", "isotopologue"])['mean_enrichment'].std()
        df_full = pd.concat([df_replicate_mean, df_replicate_std], axis=1)
        df_full.columns = ("mean", "std")

        # Nous formattons le df pour pouvoir plotter
        df_ready = df_full.unstack()
        df_ready.sort_index(level="condition_order", inplace=True)
        df_ready = df_ready.droplevel(level="condition_order")

        # Passons au plot
        sns.set_context("poster")
        this_ax = df_ready["mean"].plot.bar(yerr=df_ready['std'],
                                            figsize=(30, 15),
                                            title=self.metabolite,
                                            color=cc.glasbey_dark[3])
        this_ax.set_xlabel('Condition, Time and Replicate')
        this_ax.set_ylabel('mean_enrichment')
        this_ax.set_xticklabels(this_ax.get_xticklabels(),
                                rotation=45,
                                horizontalalignment='right')
        this_ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
        plt.xticks(rotation=45)
        plt.tight_layout()
        plt.savefig(self.static_fig_name, bbox_inches='tight', format=self.fmt)
        if self.display:
            plt.show()
        else:
            plt.close()


class InteractivePlot(Plot):
    """Class to generate the different interactive plots"""

    def __init__(self, stack, value, data, name, metabolite, condition, time):

        super().__init__(stack, value, data, name, metabolite, condition, time)
        self.filename = self.metabolite + ".html"
        self.plot_tools = "save, wheel_zoom, reset, hover, pan"

    def mean_enrichment_plot(self):
        """Generate interactive mean_enrichment plots"""

        output_file(filename=self.metabolite + ".html", title=self.metabolite)

        # Nous filtrons les données en fonction des paramètres du dashboard
        tmpdf = self.filtered_data

        # Nous préparons une liste et un df dans lesquels on va ajouter les mean_enrichment
        list_of_tmpdfs = []

        # Nous retirons les duplicats pour chaque ID individuel
        for ID in tmpdf["ID"].drop_duplicates():
            tmpdf2 = tmpdf[tmpdf["ID"] == ID].drop_duplicates(subset=['mean_enrichment'])
            list_of_tmpdfs.append(tmpdf2)

        # Nous mettons l'ordre de conditions en fonction du template, et nous préparons les datas pour plotter
        mean_enrichment_df = pd.concat(list_of_tmpdfs, ignore_index=True)

        del list_of_tmpdfs

        mean_enrichment_df = mean_enrichment_df[["ID", "condition_order", "mean_enrichment"]]
        mean_enrichment_df.sort_values(by="condition_order", inplace=True)
        mean_enrichment_df.drop(labels="condition_order", axis=1, inplace=True)
        mean_enrichment_df.set_index("ID", inplace=True)

        my_x_range = mean_enrichment_df.index.tolist()
        values = mean_enrichment_df["mean_enrichment"].tolist()
        conditions, times, replicates = Plot.split_ids(my_x_range)
        source = bk.models.ColumnDataSource(dict(x=my_x_range, y=values,
                                                 conds=conditions,
                                                 times=times,
                                                 reps=replicates))

        TOOLTIPS = [
            ("Condition", "@conds"),
            ("Time", "@times"),
            ("Replicate", "@reps"),
            ("Value", "@y")
        ]

        myplot = figure(plot_width=self.WIDTH,
                        plot_height=self.HEIGHT,
                        title=self.name,
                        y_axis_label="mean_enrichment",
                        tools=self.plot_tools,
                        tooltips=TOOLTIPS,
                        x_range=my_x_range)

        myplot.vbar(width=0.9,
                    bottom=0,
                    top="y",
                    color=cc.glasbey_dark[3],
                    x="x",
                    source=source)

        myplot.xaxis.major_label_orientation = math.pi / 4
        show(myplot)

    def mean_enrichment_meanplot(self):
        """Generate interactive mean_enrichment plots with meaned replicates"""

        output_file(filename=self.metabolite + ".html", title=self.metabolite)

        # Nous filtrons les données en fonction des paramètres du dashboard
        tmpdf = self.filtered_data

        df_replicate_mean = tmpdf.groupby(
            ["condition_order", "condition", "time", "isotopologue"])[self.value].mean()
        df_replicate_std = tmpdf.groupby(
            ["condition_order", "condition", "time", "isotopologue"])[self.value].std()
        mean_df = df_replicate_mean.to_frame()
        std_df = df_replicate_std.to_frame()

        # Ici nous mettons les moyennes dans un df et nous trions dans l'ordre demandé dans le template
        mean_df_unstack = mean_df.unstack()
        mean_df_unstack.sort_index(level="condition_order", inplace=True)
        mean_df_unstack = mean_df_unstack.droplevel(level=0)
        mean_df_unstack.columns = mean_df_unstack.columns.droplevel(level=0)
        mean_df_unstack.columns = mean_df_unstack.columns.astype(str)
        mean_df_unstack.index = ['{}_{}'.format(i, j) for i, j in mean_df_unstack.index]  # Nous recréons la colonne ID

        # Les colonnes contenants toutes les mêmes valeurs, nous pouvons juste prendre la première
        mean_series = mean_df_unstack.iloc[:, 0].copy()

        # Ici nous mettons les SD dans un df et nous trions dans l'ordre demandé dans le template
        std_df_unstack = std_df.unstack()
        std_df_unstack.sort_index(level="condition_order", inplace=True)
        std_df_unstack = std_df_unstack.droplevel(level=0)
        std_df_unstack.columns = std_df_unstack.columns.droplevel(level=0)
        std_df_unstack.columns = std_df_unstack.columns.astype(str)
        std_df_unstack.index = ['{}_{}'.format(i, j) for i, j in std_df_unstack.index]

        # Les colonnes contenants toutes les mêmes valeurs, nous pouvons juste prendre la première
        std_series = std_df_unstack.iloc[:, 0].copy()

        # Nous préparons les hauts et bas pour placer les barres d'erreur
        upper_series = mean_series.add(std_series, fill_value=0)
        upper_list = upper_series.to_list()

        lower_series = mean_series.sub(std_series, axis='index', fill_value=0)
        lower_list = lower_series.to_list()

        # Nous préparons les datas pour plotter
        my_x_range = mean_series.index.tolist()
        values = mean_series.to_list()
        conditions, times, replicates = Plot.split_ids(my_x_range)
        my_dict = dict(ID=my_x_range, tops=values,
                       conds=conditions, times=times,
                       reps=replicates)
        source = bk.models.ColumnDataSource(my_dict)

        # Nous préparons le dictionnaire qui va faire le ColumnDataSource pour les barres d'erreur
        whisker_dico = dict(base=my_x_range, upper=upper_list, lower=lower_list)

        # Passons au plot
        TOOLTIPS = [
            ("Condition", "@conds"),
            ("Time", "@times"),
            ("Replicate", "@reps"),
            ("Value", "@tops")
        ]

        myplot = figure(plot_width=self.WIDTH,
                        plot_height=self.HEIGHT,
                        title=self.name,
                        y_axis_label="mean_enrichment",
                        tools=self.plot_tools,
                        tooltips=TOOLTIPS,
                        x_range=my_x_range)

        myplot.vbar(width=0.9,
                    bottom=0,
                    top='tops',
                    color=cc.glasbey_dark[3],
                    x='ID',
                    source=source)

        mywhisker = bk.models.Whisker(source=bk.models.ColumnDataSource(whisker_dico),
                                      base="base",
                                      upper="upper",
                                      lower="lower",
                                      level="overlay")

        # Nous ajoutons les barres d'erreurs
        myplot.add_layout(mywhisker)

        myplot.xaxis.major_label_orientation = math.pi / 4
        show(myplot)

    def stacked_barplot(self):
        """Generate interactive stacked barplots"""

        output_file(filename=self.metabolite + ".html", title=self.metabolite)

        # Nous filtrons les datas à plotter
        mydata = self.filtered_data
        mydatapivot = mydata.pivot_table(index=["condition_order", 'ID'],
                                         columns='isotopologue',
                                         values=self.value)
        mydatapivot.sort_index(level="condition_order", inplace=True)
        mydatapivot = mydatapivot.droplevel(level="condition_order")
        mydatapivot.columns = mydatapivot.columns.astype(str)

        # préparons les différentes couches des barres à stacker
        stackers = mydatapivot.columns.tolist()

        # Récupérons les noms à mettre en x
        my_x_range = mydatapivot.index.tolist()

        # Nous récupérons les noms pour les tooltips
        conditions, times, replicates = Plot.split_ids(my_x_range)

        # Nous faisons ici des listes avec les données pour chaque couche (une couche=un isotopologue)
        listoflists = [mydatapivot[val].tolist() for val in stackers]

        # Nous mettons ça dans un dictionnaire pour le ColumnDataSource
        myplotdic = dict(zip(stackers, listoflists))
        myplotdic.update({'ID': my_x_range,
                          'conds': conditions,
                          'times': times,
                          'reps': replicates})

        # Préparation des tooltips
        TOOLTIPS = [
            ("Condition", "@conds"),
            ('Time', '@times'),
            ('Replicate', '@reps'),
            ("Isotopologue", "$name"),
            ("Value", "@$name"),
        ]

        # Initialization de la figure
        myplot = figure(
            x_range=my_x_range,
            plot_width=self.WIDTH,
            plot_height=self.HEIGHT,
            title=self.name,
            y_axis_label=self.value,
            tools=self.plot_tools,
            tooltips=TOOLTIPS
        )

        # Passons au plot
        myplot.vbar_stack(stackers,
                          x='ID',
                          source=myplotdic,
                          width=0.9,
                          color=cc.glasbey_dark[:len(stackers)]
                          )

        myplot.xaxis.major_label_orientation = math.pi / 4

        show(myplot)

    def unstacked_barplot(self):
        """Generate interactive unstacked barplots"""

        output_file(filename=self.metabolite + ".html", title=self.metabolite)

        # Nous filtrons les datas à plotter
        mydata = self.filtered_data
        mydatapivot = mydata.pivot_table(index=["condition_order", 'ID'],
                                         columns='isotopologue',
                                         values=self.value)
        mydatapivot.sort_index(level="condition_order", inplace=True)
        mydatapivot = mydatapivot.droplevel(level="condition_order")

        # Nous récupérons les colonnes pour faire les couches à stacker
        stackers = mydatapivot.columns.astype(str).tolist()

        # Nous faisons des tuples avec index et couche à plotter
        factors = [(i, stack) for i in mydatapivot.index for stack in stackers]
        condition_time = [i[0] for i in factors]
        isotops = [i[1] for i in factors]

        # Nous récupérons les noms pour les tooltips
        conditions, times, replicates = Plot.split_ids(condition_time)

        # Nous récupérons les valeurs de chaque couche
        tops = [row[int(stack) + 1]
                for row in mydatapivot.itertuples()
                for stack in stackers]

        # Nous mettons tout ça dans un ColumnDataSource
        source = bk.models.ColumnDataSource(data=dict(
            x=factors, tops=tops, conds=conditions,
            times=times, reps=replicates, isotops=isotops))

        # Préparation des tooltips
        TOOLTIPS = [
            ("Condition", "@conds"),
            ("Time", '@times'),
            ("Replicate", "@reps"),
            ('Isotopologue', '@isotops'),
            ("Value", "@tops")
        ]

        # Initialisation de la figure
        plot = figure(
            x_range=bk.models.FactorRange(*factors),  # Voir docu sur Bokeh.org pour ça
            plot_width=self.WIDTH,
            plot_height=self.HEIGHT,
            tools=self.plot_tools,
            tooltips=TOOLTIPS)

        # Passons au plot
        plot.vbar(x='x',
                  top='tops',
                  width=0.9,
                  source=source,
                  fill_color=bk.transform.factor_cmap('x',
                                                      palette=cc.glasbey_dark,
                                                      factors=stackers,
                                                      start=1, end=2),
                  line_color="white")

        plot.xaxis.major_label_orientation = math.pi / 4
        plot.y_range.start = 0
        plot.x_range.range_padding = 0.1
        show(plot)

    def stacked_meanplot(self):
        """Generate interactive stacked barplots with meaned replicates"""

        output_file(filename=self.metabolite + ".html", title=self.metabolite)

        # Nous filtrons les datas à plotter et préparons les moyennes et SD
        tmpdf = self.filtered_data
        df_replicate_mean = tmpdf.groupby(
            ["condition_order", "condition", "time", "isotopologue"])[self.value].mean()
        df_replicate_std = tmpdf.groupby(
            ["condition_order", "condition", "time", "isotopologue"])[self.value].std()
        mean_df = df_replicate_mean.to_frame()
        std_df = df_replicate_std.to_frame()

        # Ici nous mettons les moyennes et SD dans des df
        mean_df_unstack = mean_df.unstack()
        mean_df_unstack.sort_index(level="condition_order", inplace=True)
        mean_df_unstack = mean_df_unstack.droplevel(level=0)
        mean_df_unstack.columns = mean_df_unstack.columns.droplevel(level=0)
        mean_df_unstack.columns = mean_df_unstack.columns.astype(str)
        mean_df_unstack.index = ['{}_{}'.format(i, j) for i, j in mean_df_unstack.index]

        std_df_unstack = std_df.unstack()
        std_df_unstack.sort_index(level="condition_order", inplace=True)
        std_df_unstack = std_df_unstack.droplevel(level=0)
        std_df_unstack.columns = std_df_unstack.columns.droplevel(level=0)
        std_df_unstack.columns = std_df_unstack.columns.astype(str)
        std_df_unstack.index = ['{}_{}'.format(i, j) for i, j in std_df_unstack.index]

        upper_df = mean_df_unstack.add(std_df_unstack, fill_value=0)
        upper_df.columns = upper_df.columns.astype(str)
        lower_df = mean_df_unstack.sub(std_df_unstack, axis='index', fill_value=0)
        lower_df.columns = lower_df.columns.astype(str)

        stackers = mean_df_unstack.columns.tolist()
        my_x_range = mean_df_unstack.index.tolist()

        # Nous initialisons les listes de valeurs moyennes isotopologue par isotopologue
        mean_listoflists = [mean_df_unstack[val].tolist() for val in stackers]

        meanplotdic = dict(zip(stackers, mean_listoflists))
        meanplotdic.update({'ID': my_x_range})

        # Nous récupérons les noms pour les tooltips
        conditions, times = Plot.split_ids(my_x_range)

        meanplotdic.update({'conds': conditions,
                            'times': times})

        # Préparation des tooltips
        TOOLTIPS = [
            ("Condition", "@conds"),
            ("Time", "@times"),
            ("Isotopologue", "$name"),
            ("Value", "@$name"),
        ]

        # Initialization de la figure
        myplot = figure(
            x_range=my_x_range,
            plot_width=self.WIDTH,
            plot_height=self.HEIGHT,
            title=self.name,
            y_axis_label=self.value,
            tools=self.plot_tools,
            tooltips=TOOLTIPS
        )

        colors = cc.glasbey_dark[:len(stackers)]

        # Passons au plot
        myplot.vbar_stack(stackers,
                          x='ID',
                          source=meanplotdic,
                          width=0.9,
                          color=colors
                          )

        # Nous préparons les données pour positionner les barres d'erreur
        dico_upper_list = []
        dico_lower_list = []

        for _ in upper_df[stackers[0]]:
            dico_upper_list.append(0)
            dico_lower_list.append(0)

        dico_upper_array = np.array(dico_upper_list)
        dico_lower_array = np.array(dico_lower_list)

        # Nous faisons la suite couche par couche
        for val in stackers:
            base = my_x_range

            # Nous devons incrementer à chaque loop la valeur pour la barre d'erreur du dessous pour avoir la bonne
            # hauteur de placement
            upper_listoflists = list(upper_df[val])
            lower_listoflists = list(lower_df[val])
            upper_array = np.array(upper_listoflists)
            lower_array = np.array(lower_listoflists)
            dico_upper_array = np.add(dico_upper_array, upper_array)
            dico_lower_array = np.add(dico_lower_array, lower_array)
            dico_lower_list = dico_lower_array.tolist()
            dico_upper_list = dico_upper_array.tolist()
            whisker_dico = dict(base=base, upper=dico_upper_list, lower=dico_lower_list)
            source = bk.models.ColumnDataSource(data=whisker_dico)
            mywhisker = Whisker(source=source, base="base", upper="upper", lower="lower", level="overlay")

            # Nous ajoutons les barres d'erreurs (couche par couche)
            myplot.add_layout(mywhisker)

        myplot.xaxis.major_label_orientation = math.pi / 4
        show(myplot)

    def unstacked_meanplot(self):
        """Generate interactive unstacked barplots with meaned replicates"""

        output_file(filename=self.metabolite + ".html", title=self.metabolite)

        # Préparation des datas à plotter
        tmpdf = self.filtered_data
        df_replicate_mean = tmpdf.groupby(
            ["condition_order", "condition", "time", "isotopologue"])[self.value].mean()
        df_replicate_std = tmpdf.groupby(
            ["condition_order", "condition", "time", "isotopologue"])[self.value].std()
        mean_df = df_replicate_mean.to_frame()
        std_df = df_replicate_std.to_frame()

        mean_df_unstack = mean_df.unstack()
        mean_df_unstack.sort_index(level="condition_order", inplace=True)
        mean_df_unstack = mean_df_unstack.droplevel(level=0)
        mean_df_unstack.columns = mean_df_unstack.columns.droplevel(level=0)
        mean_df_unstack.columns = mean_df_unstack.columns.astype(str)
        mean_df_unstack.index = ['{}_{}'.format(i, j) for i, j in mean_df_unstack.index]

        std_df_unstack = std_df.unstack()
        std_df_unstack.sort_index(level="condition_order", inplace=True)
        std_df_unstack = std_df_unstack.droplevel(level=0)
        std_df_unstack.columns = std_df_unstack.columns.droplevel(level=0)
        std_df_unstack.columns = std_df_unstack.columns.astype(str)
        std_df_unstack.index = ['{}_{}'.format(i, j) for i, j in std_df_unstack.index]
        stackers = mean_df_unstack.columns.tolist()

        upper_df = mean_df_unstack.add(std_df_unstack, fill_value=0)
        lower_df = mean_df_unstack.sub(std_df_unstack, axis='index', fill_value=0)

        # Même principe que pour les non stackés non moyennés sauf que nous préparons les barres d'erreur aussi
        factors = [(i, stack) for i in mean_df_unstack.index for stack in stackers]
        condition_time = [i[0] for i in factors]
        isotops = [i[1] for i in factors]
        conditions, times = Plot.split_ids(condition_time)

        tops = [row[int(stack) + 1]
                for row in mean_df_unstack.itertuples()
                for stack in stackers]

        upper_list = [row[int(stack) + 1]
                      for row in upper_df.itertuples()
                      for stack in stackers]

        lower_list = [row[int(stack) + 1]
                      for row in lower_df.itertuples()
                      for stack in stackers]

        base = factors
        source = bk.models.ColumnDataSource(data=dict(
            x=factors, tops=tops, conds=conditions,
            times=times, isotops=isotops))

        # Nous faisons un ColumnDataSource pour les incertitudes
        source_error = bk.models.ColumnDataSource(
            data=dict(base=base, upper=upper_list, lower=lower_list))

        # Préparation des tooltips
        TOOLTIPS = [
            ("Condition", "@conds"),
            ("Time", "@times"),
            ("Isotopologue", "@isotops"),
            ("Fraction", "@tops")
        ]

        # Initialisation de la figure
        plot = figure(
            x_range=bk.models.FactorRange(*factors),
            plot_width=self.WIDTH,
            plot_height=self.HEIGHT,
            tools=self.plot_tools,
            tooltips=TOOLTIPS)

        # Passons au plot
        plot.vbar(x='x',
                  top='tops',
                  width=0.9,
                  source=source,
                  fill_color=bk.transform.factor_cmap(
                      'x', palette=cc.glasbey_dark,
                      factors=stackers, start=1, end=2),
                  line_color="white")

        plot.add_layout(bk.models.Whisker(
            source=source_error,
            base="base", upper="upper",
            lower="lower", level="overlay"))
        plot.y_range.start = 0
        plot.x_range.range_padding = 0.1
        show(plot)

    def stacked_areaplot(self):
        """Generate interactive stacked areaplots"""

        output_file(filename=self.metabolite + ".html", title=self.metabolite)

        # Commençons par la préparation de data
        stackdf = self.filtered_data
        stackpivot = stackdf.pivot(index='ID', columns='isotopologue', values=self.value)
        stackpivot.columns = stackpivot.columns.astype(str)
        mysource = bk.models.ColumnDataSource(data=stackpivot)
        mystackers = stackpivot.columns.tolist()
        colors = cc.glasbey_dark[:len(mystackers)]

        TOOLTIPS = [
            ("", "<strong>@ID</strong>"),
            ("isotopologue", "$name"),
            ("ID", "@$name"),
        ]

        plot = figure(
            width=self.WIDTH,
            height=self.HEIGHT,
            tools=self.plot_tools,
            tooltips=TOOLTIPS,
            x_range=stackpivot.index.values
        )

        plot.xaxis.major_label_orientation = math.pi / 4
        plot.varea_stack(mystackers, x="ID", color=colors, source=mysource)
        show(plot)


class Map:
    """
    Class to create maps from Isocor output (MS data from C13 labelling experiments)

    :param annot: Should annotations be apparent on map or not
    :type annot: Bool
    """

    def __init__(self, data, name, annot, fmt, display=False):
        """

        """
        self.data = data
        self.name = name
        self.annot = annot
        self.fmt = fmt
        self.display = display

        # Il faut préparer les données pour les maps:
        self.heatmapdf = self.data[
            ['metabolite', 'mean_enrichment',
             'condition', 'time', 'number_rep']]
        self.heatmapdf = self.heatmapdf[self.heatmapdf['mean_enrichment'] != 0]
        self.heatmapdf.drop_duplicates(inplace=True)
        self.heatmapdf.dropna(inplace=True)
        self.heatmapdf.set_index('metabolite', inplace=True)
        self.heatmapdf['Condition_Time'] = self.heatmapdf['condition'].apply(str) + '_T' + self.heatmapdf['time'].apply(
            str)
        self.heatmapdf = self.heatmapdf.groupby(
            ['metabolite', 'Condition_Time'])['mean_enrichment'].mean()
        self.heatmapdf = self.heatmapdf.unstack(0)
        self.dc_heatmap = self.heatmapdf.describe(include='all')
        self.heatmap_center = self.dc_heatmap.iloc[5, 0].mean()
        self.clustermapdf = self.heatmapdf.fillna(value=0)

    def build_heatmap(self):
        """
        Create a heatmap of mean_enrichment data across
        all conditions & times & metabolites
        """

        fig, ax = plt.subplots(figsize=(30, 30))
        sns.set(font_scale=1)
        sns.heatmap(self.heatmapdf, vmin=0.02,
                    robust=True, center=self.heatmap_center,
                    annot=self.annot, fmt="f", linecolor='black',
                    linewidths=.2, cmap='Blues', ax=ax)
        plt.yticks(rotation=0, fontsize=20)
        plt.xticks(rotation=45, fontsize=20)
        # bottom, top = ax.get_ylim()
        plt.savefig(self.name + '.' + self.fmt, bbox_inches='tight', format=self.fmt)
        if self.display:
            plt.show()

    def build_clustermap(self):
        """
        Create a clustermap of mean_enrichment data across
        all conditions & times & metabolites
        """

        sns.set(font_scale=1)
        cg = sns.clustermap(self.clustermapdf,
                            cmap="Blues", fmt="f",
                            linewidths=.2, standard_scale=1,
                            figsize=(30, 30), linecolor='black',
                            annot=self.annot)
        plt.setp(cg.ax_heatmap.yaxis.get_majorticklabels(), rotation=0, fontsize=20)
        plt.setp(cg.ax_heatmap.xaxis.get_majorticklabels(), rotation=45, fontsize=20)
        plt.savefig(self.name + '.' + self.fmt, bbox_inches='tight', format=self.fmt)
        if self.display:
            plt.show()

    def build_interactive_heatmap(self):
        """
        Create an interactive heatmap of mean_enrichment data across
        all conditions & times & metabolites
        """

        output_file(filename=self.name + ".html", title=self.name + ".html")

        condition_time = list(self.heatmapdf.index.astype(str))
        metabolites = list(self.heatmapdf.columns)

        # Nous réordonnons les données
        df = pd.DataFrame(self.heatmapdf.stack(), columns=["values"]).reset_index()

        # Nous préparons les couleurs
        colors = cc.kbc[len(df)]
        mapper = LinearColorMapper(palette=cc.kbc[:len(df)])

        TOOLTIPS = "hover,save"

        # initialisation de la figure
        p = figure(title=self.name,
                   x_range=list(reversed(metabolites)), y_range=condition_time,
                   x_axis_location="below", plot_width=1080, plot_height=640,
                   tools=TOOLTIPS, toolbar_location='above',
                   tooltips=[('datapoint', '@metabolite @Condition_Time'), ('value', "@values")])

        p.grid.grid_line_color = None
        p.axis.axis_line_color = None
        p.axis.major_tick_line_color = None
        p.axis.major_label_text_font_size = "15px"
        p.axis.major_label_standoff = 0
        p.xaxis.major_label_orientation = math.pi / 3

        # Passons au plot
        p.rect(x="metabolite",
               y="Condition_Time",
               width=1, height=1,
               source=df,
               fill_color={'field': "values", 'transform': mapper},
               line_color=None)

        # Nous préparons la barre de couleur pour la légende
        color_bar = ColorBar(color_mapper=mapper, major_label_text_font_size="7px",
                             ticker=BasicTicker(desired_num_ticks=len(colors)),
                             formatter=PrintfTickFormatter(),
                             label_standoff=6, border_line_color=None, location=(0, 0))

        # Ajoutons la barre de couleur de la légende
        p.add_layout(color_bar, 'right')

        show(p)
