# -*- coding: utf-8 -*-


# Import necessary libraries
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import permutation_test_score
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
from sklearn.utils import shuffle
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.offline as pyo
from .cross_validation import CrossValidation
#import plotting
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

import warnings
warnings.filterwarnings('ignore')

from pyChemometrics import ChemometricsScaler

import os

__auther__ = "aeiwz"


class opls_da:
    
    
        
    # Import necessary libraries
    import numpy as np
    import pandas as pd
    from sklearn.model_selection import train_test_split, cross_val_score
    from sklearn.metrics import accuracy_score, roc_auc_score
    from sklearn.preprocessing import StandardScaler
    from sklearn.pipeline import Pipeline
    from sklearn.decomposition import PCA
    from sklearn.cross_decomposition import PLSRegression
    from sklearn.model_selection import permutation_test_score
    import matplotlib.pyplot as plt
    from sklearn.metrics import r2_score
    from sklearn.utils import shuffle
    import plotly.express as px
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    import plotly.offline as pyo
    from .cross_validation import CrossValidation
    #import plotting
    from sklearn.linear_model import LinearRegression
    from sklearn.metrics import mean_squared_error

    import warnings
    warnings.filterwarnings('ignore')

    from pyChemometrics import ChemometricsScaler

    import os




    '''
    OPLS-DA model
    
    Parameters
    ----------
    X : array-like, shape (n_samples, n_features)
        Training data, where n_samples is the number of samples and n_features is the number of features.
    y : array-like, shape (n_samples,)
        Target data, where n_samples is the number of samples.
    n_components : int, default=2
        Number of components to keep.
    scale : str, default='par'
        Method of scaling. 'par' for pareto scaling, 'mc' for mean centering, 'uv' for unitvarian scaling.
    cv : int, default=5
        Number of cross-validation folds.
    n_permutations : int, default=1000
        Number of permutations for permutation test.
    random_state : int, default=42
        Random state for permutation test.
    kfold : int, default=3
        Number of cross-validation folds.


    Examples:
    ----------
    
        ## Import package into python


        from metbit import opls_da, pca
        import pandas as pd
        import numpy as np


        ## Load dataset
        For example dataset are generated by random


        data = pd.DataFrame(np.random.rand(500, 50000))
        class_ = pd.Series(np.random.choice(['A', 'B'], 500), name='Group')

        datasets = pd.concat([class_, data], axis=1)


        # Assign X and target
        X = datasets.iloc[:, 2:]
        y = datasets['Group']
        time = datasets['Time point']
        features_name = list(X.columns.astype(float))

        ## Perform OPLS-DA model


        opls_da_mod = opls_da(X=X, y=y,features_name=features_name, n_components=2, scale='pareto', kfold=3, estimator='opls', random_state=42):
                
        opls_da.fit()

        opls.permutation_test(n_permutataion=1000,cv=3, n_jobs=-1, verbose=10)

        opls_da.vip_scores()


        ## Isualiseation of OPLs-DA model



        opls_da_model.plot_oplsda_scores()

        opls_da_model.vip_plot()

        opls_da_model.plot_hist()

        opls_da_model.plot_s_scores()

        opls_da_model.plot_loading()


        

    '''
    

        
    
    def __init__(self, X, y,features_name=None, n_components=2, scale='pareto', kfold=3, estimator='opls', random_state=42):
        


        #check X and y must be dataframe or array
        if not isinstance(X, (pd.DataFrame, np.ndarray)):
            raise ValueError('X must be a dataframe or array')
        if not isinstance(y, (pd.Series, np.ndarray)):
            raise ValueError('y must be a series or array')
        if X.shape[0] != y.shape[0]:
            raise ValueError('X and y must have the same number of samples')
        if not isinstance(n_components, int):
            raise ValueError('n_components must be an integer')
        if not isinstance(scale, str):
            raise ValueError('scale must be a string')
        if not isinstance(kfold, int):
            raise ValueError('kfold must be an integer')
        if not isinstance(estimator, str):
            raise ValueError('estimator must be a string')
        if not isinstance(random_state, int):
            raise ValueError('random_state must be an integer')
        if features_name is not None:
            if not isinstance(features_name, (pd.Series, np.ndarray, list)):
                raise ValueError('features_name must be a series, list or 1D array')
            if len(features_name) != X.shape[1]:
                raise ValueError('features_name must have the same number of features as X')
            
            
            
        #check unique values in y
        if isinstance(y, pd.Series):
            if len(y.unique()) < 2:
                raise ValueError('OPLS-DA requires at least 2 group comparisons')
        if isinstance(y, np.ndarray):
            if len(np.unique(y)) < 2:
                raise ValueError('OPLS-DA requires at least 2 group comparisons')
        if isinstance(y, list):
            if len(np.unique(y)) < 2:
                raise ValueError('OPLS-DA requires at least 2 group comparisons')        
            
        #check unique values in y
        if isinstance(y, pd.Series):
            if len(y.unique()) > 2:
                raise ValueError('OPLS-DA requires only 2 group comparisons')
        if isinstance(y, np.ndarray):
            if len(np.unique(y)) < 2:
                raise ValueError('OPLS-DA requires only 2 group comparisons')
        if isinstance(y, list):
            if len(np.unique(y)) < 2:
                raise ValueError('OPLS-DA requires only 2 group comparisons')



        self.features_name = features_name
        if features_name is None:
            if isinstance(X, pd.DataFrame):
                self.features_name = X.columns
            else:
                self.features_name = np.arange(X.shape[1])
        else:
            self.features_name = features_name
            
        if isinstance(X, pd.DataFrame):
            self.X = X.values
        else:
            self.X = X 

        #fill nan with 0
        self.X = np.nan_to_num(self.X)
                     
        self.X = X
        self.y = y
        self.n_components = n_components
        self.scale = scale
        self.random_state = random_state
        self.opls_model = None
        self.opls_cv = None
        self.opls_permutation_cv = None
        self.opls_permutation_cv_scores = None
        self.opls_permutation_cv_score = None
        self.opls_permutation_cv_score_std = None
        self.kfold = kfold
        self.estimator = estimator
        
        
    def fit(self):
        
        X = self.X
        y = self.y
        features_name = self.features_name
        n_components = self.n_components
        scale = self.scale

        random_state = self.random_state
        kfold = self.kfold
        estimator = self.estimator
        
        if scale == 'pareto':
            scale_power = 0.5
        elif scale == 'mean':
            scale_power = 0
        elif scale == 'uv':
            scale_power = 1
        elif scale == 'minmax':
            scale_power = 0
            
        self.scale = scale
            
            
        # Create a pipeline with data preprocessing and OPLS-DA model
        pipeline = Pipeline([
                                ('scale', ChemometricsScaler(scale_power=scale_power)),
                                ('oplsda', PLSRegression(n_components=n_components)),
                                ('opls', CrossValidation(kfold=kfold, estimator=estimator, scaler=scale))
                            ])

        self.pipeline = pipeline

        oplsda = pipeline.named_steps['oplsda']
        cv = pipeline.named_steps['opls']
        cv.fit(X.values, y)

        oplsda.fit(X, pd.Categorical(y).codes)
        
        s_df_scores_ = pd.DataFrame({'correlation': cv.correlation,'covariance': cv.covariance}, index=features_name)
        df_opls_scores = pd.DataFrame({'t_scores': cv.scores, 't_ortho': cv.orthogonal_score, 't_pred': cv.predictive_score, 'Group': y})


        R2Xcorr = cv.R2Xcorr
        R2y = cv.R2y
        q2 = cv.q2

        self.R2Xcorr = R2Xcorr
        self.R2y = R2y
        self.q2 = q2

        self.s_df_scores_ = s_df_scores_
        self.df_opls_scores = df_opls_scores
        
        self.oplsda = oplsda
        self.cv = cv
    


    def permutation_test(self, n_permutations=500, cv=3, n_jobs=-1, verbose=10):


        from sklearn.pipeline import Pipeline
        
        self.cv = cv
        self.n_permutations = n_permutations
        oplsda = self.oplsda
        X = self.X
        y = self.y
        pipeline = self.pipeline
        randomstate = self.random_state
        self.n_jobs = n_jobs
        self.verbose = verbose
        
        oplsda.fit(X, pd.Categorical(y).codes)

        # Permutation test to assess the significance of the model
        acc_score, permutation_scores, p_value = permutation_test_score(
        pipeline.named_steps['oplsda'], X, pd.Categorical(y).codes, cv=3, n_permutations=n_permutations, n_jobs=n_jobs, random_state=randomstate, verbose=verbose)


        self.acc_score = acc_score
        self.permutation_scores = permutation_scores
        self.p_value = p_value
        
        
    
    def vip_scores(self, model=None, features_name = None):
        
        
        if model is None:   
            model = self.oplsda
        else:
            model = model
            
        
        features_name = self.features_name



        t = model.x_scores_
        w = model.x_weights_
        q = model.y_loadings_
        p, h = w.shape
        vips = np.zeros((p,))
        s = np.diag(t.T @ t @ q.T @ q).reshape(h, -1)
        total_s = np.sum(s)
        for i in range(p):
            weight = np.array([ (w[i,j] / np.linalg.norm(w[:,j]))**2 for j in range(h) ])
            vips[i] = np.sqrt(p*(s.T @ weight)/total_s)
       
        if features_name is not None:
            vips = pd.DataFrame(vips, columns = ['VIP'])
            vips['Features'] = features_name
        else:
            vips = pd.DataFrame(vips, columns = ['VIP'])
            vips['Features'] = vips.index



        self.vips = vips

        return

    def get_vip_scores(self):
        vips = self.vips
        return vips



    def vip_plot(self, x_range = 9, threshold = 2, size = 12, width = 1000, height = 500, filter_ = False):
        
        
        # add scatter plot of VIP score
        import plotly.express as px
        vips = self.vips

        #add threshold column to define cutoff for VIP score if >= treschold then 1 else 0
        vips['threshold'] = np.where(vips['VIP'] >= threshold, "Pass", "Under cut off")

        if filter_ == True:
            vips = vips[vips['VIP'] >= threshold]

        fig = px.scatter(vips, x='Features', y='VIP', 
        text='Features', 
        color='threshold', color_discrete_map={'Pass':'#FF7961', 'Under cut off':'#ECECEC'}, 
        height=height, width=width, 
        title='VIP score')

        fig.update_traces(marker=dict(size=size))
        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')

        # tickformat of y-axis to 2 decimal places and x-axis to 3 decimal places
        fig.update_yaxes(tickformat=".2f")
        fig.update_xaxes(tickformat=".3f")

        
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))

        # reverse the x-axis
        fig.update_xaxes(autorange="reversed")
        
        # add dashed line for threshold
        fig.add_shape(type="line",
                    x0=0, y0=threshold, x1=x_range, y1=threshold,
                    line=dict(color="red",width=2))
                    
        fig.update_layout(showlegend=False)
        
        return fig





    def plot_oplsda_scores(self, color_dict = None, symbol = None, symbol_dict = None, fig_height = 900, fig_width = 1300,
                    marker_size = 35, marker_opacity = 0.7):
        
        #Visualise
        #check symbol dimension must be equal to y
        if symbol is not None:
            if len(symbol) != len(self.y):
                raise ValueError('symbol must have the same number of samples as y')

        #check symbol_dict must be a dictionary
        if symbol_dict is not None:
            if not isinstance(symbol_dict, dict):
                raise ValueError('symbol_dict must be a dictionary')
        else:
            symbol_dict = None

        #check color_dict must be a dictionary
        if color_dict is not None:
            if not isinstance(color_dict, dict):
                raise ValueError('color_dict must be a dictionary')
        else:
            color_dict = None

        df_opls_scores = self.df_opls_scores


        
        from .pca_ellipse import confidence_ellipse
        fig = px.scatter(df_opls_scores, x='t_scores', y='t_ortho', symbol=symbol, 
                    
                        symbol_map=symbol_dict,
                    
                        color='Group', 
                        color_discrete_map=color_dict, 
                        title='<b>OPLS-DA Scores Plot<b>', 
                        height=fig_height, width=fig_width,
                        labels={
                            't_pred': 't<sub>predict</sub>',
                            't_ortho': 't<sub>orthogonal</sub>',
                            't_scores': 't<sub>scores</sub>',
                            'Group': 'Group'}
        )

        fig.update_traces(marker=dict(size=marker_size, 
                            opacity=marker_opacity, 
                            line=dict(width=2, color='DarkSlateGrey')))


        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=0,
                                y=1.04+0.05,
                                showarrow=False,
                                text='<b>R<sup>2</sup>X: {}%<b>'.format(np.round(self.R2Xcorr*100, decimals=2)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")


        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=0,
                                y=1.0+0.05,
                                showarrow=False,
                                text='<b>R<sup>2</sup>Y: {}%<b>'.format(np.round(self.R2y*100, decimals=2)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")


        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=0,
                                y=1.08+0.05,
                                showarrow=False,
                                text='<b>Q<sup>2</sup>: {}%<b>'.format(np.round(self.q2*100, decimals=2)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

                        
        fig.add_shape(type='path',
                path=confidence_ellipse(df_opls_scores['t_scores'], df_opls_scores['t_ortho']))


        fig.update_yaxes(tickformat=",.0")
        fig.update_xaxes(tickformat=",.0")

        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))
        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')

        return fig



    def plot_hist(self, nbins_=50, height_=500, width_=1000):

        #check permutation model must be fitted
        if self.permutation_scores is None:
            raise ValueError('Permutation test must be performed first')

        permutation_scores = self.permutation_scores


        #Histrogram
        #Plot histogram of permutation scores
        fig = px.histogram(permutation_scores, nbins=nbins_, height=height_, width=width_, 
                        title='<b>Permutation scores<b>',
                        labels={'value': 'Accuracy score', 
                                'count': 'Frequency'})



        fig.add_shape(type='line', yref='paper', y0=0, y1=1, 
                        xref='x', x0=self.acc_score, x1=self.acc_score, 
                        line=dict(dash='dash', color='red', width=3))



        fig.add_annotation(dict(font=dict(color="black",size=14),
                                #x=x_loc,
                                x=0,
                                y=1.25,
                                #y=1.18,
                                showarrow=False,
                                text='Number of permutation: {}'.format(self.n_permutations),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_annotation(dict(font=dict(color="black",size=14),
                                #x=x_loc,
                                x=0,
                                y=1.18,
                                showarrow=False,
                                text='Accuracy score: {}'.format(np.round(self.acc_score, decimals=3)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")
        fig.add_annotation(dict(font=dict(color="black",size=14),
                                #x=x_loc,
                                x=0,
                                y=1.11,
                                showarrow=False,
                                text='<i>p-value</i>: {}'.format(np.round(self.p_value, decimals=6)),
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")


        
        fig.update_layout(showlegend=False)

        fig.update_layout(title_x=0.5)

        return fig



    def plot_s_scores(self, height_=900, width_=2000, range_color_=[-0.05,0.05], color_continuous_scale_='jet'):

        s_df_scores_ = self.s_df_scores_


        fig = px.scatter(s_df_scores_, x='covariance', y='correlation', color='covariance', range_color=range_color_,
                        color_continuous_scale=color_continuous_scale_, text=s_df_scores_.index, height=height_, width=width_,)
        fig.update_layout(title='<b>S-plot</b>', xaxis_title='Covariance', yaxis_title='Correlation')


        
        #add line of axis and set color to black and line width to 2 pixel
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')

        #Add tick width to 2 pixel
        fig.update_xaxes(tickwidth=2)
        fig.update_yaxes(tickwidth=2)
        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')
        fig.update_yaxes(tickformat=",.0")
        #fig.update_xaxes(tickformat=",.0")
        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))

        fig.update_traces(marker=dict(size=14))

        return fig


    
    def plot_loading(self, height_=900, width_=2000, range_color_=[-0.05,0.05], color_continuous_scale_='jet'):

        s_df_scores_ = self.s_df_scores_

        def median_corr(X):
            X_corr = np.median(X, axis=0)
            X_corr = X_corr * np.sign(s_df_scores_['correlation'])
            return X_corr

        X2 = median_corr(self.X)

        s_df_scores_ = self.s_df_scores_
        features_name = self.features_name

        fig = px.scatter(s_df_scores_, x=features_name, y=X2, 
                            color='covariance', color_continuous_scale=color_continuous_scale_, range_color=range_color_,
                            text=s_df_scores_.index, 
                            height=height_, width=width_)

        fig.update_traces(marker=dict(size=5))
        fig.update_xaxes(autorange="reversed")
        fig.update_layout(title='<b>Loading spectra</b>', xaxis_title='𝛿<sub>H</sub> in ppm', yaxis_title='Correlation')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')

        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')
        fig.update_yaxes(tickformat=",.0")
        #fig.update_xaxes(tickformat=",.0")
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))

        return fig




class pca:
    """
    Principal Component Analysis (PCA) model.

    Parameters
    ----------
    X : array-like, shape (n_samples, n_features)
        Training data, where n_samples is the number of samples and n_features is the number of features.
    label : array-like, shape (n_samples,)
        Target data, where n_samples is the number of samples.
    features_name : array-like, shape (n_features,), default=None
        Name of features.
    n_components : int, default=2
        Number of components to keep.
    scale : str, default='pareto'
        Method of scaling. 'pareto' for pareto scaling, 'mean' for mean centering, 'uv' for unitvarian scaling.
    random_state : int, default=42
        Random state for permutation test.
    test_size : float, default=0.3
        Size of test set.

    Examples
    ----------
    import pandas as pd
    import numpy as np
    from metbit import pca

    # Create a dataset
    data = pd.DataFrame(np.random.rand(500, 50000))
    class_ = pd.Series(np.random.choice(['A', 'B', 'C'], 500), name='Group')
    time = pd.Series(np.random.choice(['1-wk', '2-wk', '3-wk', '4-wk'], 500), name='Time point')

    # Assign X and target
    X = datasets.iloc[:, 2:]
    y = datasets['Group']
    time = datasets['Time point']
    features_name = list(X.columns.astype(float))

    ## Perform PCA model
    pca_mod = pca(X=X, label=y, features_name=features_name, n_components=2, scale='pareto', random_state=42, test_size=0.3)
    pca_mod.fit()

    # Visualisation of PCA model
    pca_mod.plot_observe_variance()
    pca_mod.plot_cumulative_observed()
    shape_ = {'1-wk': 'circle', '2-wk': 'square', '3-wk': 'diamond', '4-wk': 'cross'}
    pca_mod.plot_pca_scores(symbol=time, symbol_dict=shape_)
    pca_mod.plot_loading_()
    pca_mod.plot_pca_trajectory(time_=time, time_order={'1-wk': 0, '2-wk': 1, '3-wk': 2, '4-wk': 3}, color_dict={'A': '#636EFA', 'B': '#EF553B', 'C': '#00CC96'}, symbol_dict=shape_)

    """

    import numpy as np
    from sklearn import preprocessing
    import pandas as pd
    import matplotlib.pyplot as plt
    from pyChemometrics.ChemometricsPCA import ChemometricsPCA
    from pyChemometrics.ChemometricsScaler import ChemometricsScaler
    import os
    import plotly.express as px
    import plotly.graph_objects as go
    from sklearn import decomposition
    from sklearn.preprocessing import scale
    from .pca_ellipse import confidence_ellipse
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import StandardScaler
    from sklearn.decomposition import PCA
    from sklearn.metrics import r2_score
    from lingress import unipair

    def __init__(self, X, label, features_name=None, n_components=2, scale='pareto', random_state=42, test_size=0.3):
        """
        Initialize the PCA model.

        Parameters
        ----------
        X : array-like, shape (n_samples, n_features)
            Training data, where n_samples is the number of samples and n_features is the number of features.
        label : array-like, shape (n_samples,)
            Target data, where n_samples is the number of samples.
        features_name : array-like, shape (n_features,), default=None
            Name of features.
        n_components : int, default=2
            Number of components to keep.
        scale : str, default='pareto'
            Method of scaling. 'pareto' for pareto scaling, 'mean' for mean centering, 'uv' for unitvarian scaling.
        random_state : int, default=42
            Random state for permutation test.
        test_size : float, default=0.3
            Size of test set.
        """
        # Rest of the code...
        fig = px.bar(df_explained_variance_, 
                x='PC', y=df_explained_variance_['Explained variance'],
                text=np.round(df_explained_variance_['Explained variance'], decimals=3),
                width=800, height=600,
                title='Explained Variance ({} scaling)'.format(scale))
        fig.update_layout(
            title={
                'y':0.9,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=15))
        fig.update_traces(texttemplate='%{text:.3f}', textposition='outside')
        return fig


    def plot_cumulative_observed(self):
        
        df_explained_variance_ = self.df_explained_variance_

        fig = go.Figure()

        fig.add_trace(
            go.Scatter(
                x=df_explained_variance_['PC'],
                y=df_explained_variance_['Cumulative variance'],
                marker=dict(size=15, color="LightSeaGreen"),
                name='R<sup>2</sup>X (Cum)'
            ))

        fig.add_trace(
            go.Bar(
                x=df_explained_variance_['PC'],
                y=df_explained_variance_['Explained variance'],
                marker=dict(color="RoyalBlue"),
                name='R<sup>2</sup>X',
                text=np.round(df_explained_variance_['Explained variance'], decimals=3)
            ))
        fig.update_layout(width=800, height=600,
                        title='Explained Variance and Cumulative Variance')
        fig.update_layout(
            title={
                'y':0.9,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'})

        return fig



    def plot_pca_scores(self, pc=['PC1', 'PC2'], 
                        color_=None, color_dict=None, 
                        symbol=None, symbol_dict=None, 
                        fig_height=900, fig_width=1300,
                        marker_size=35, marker_opacity=0.7,
                        text_ = None):

        from .pca_ellipse import confidence_ellipse

        scale = self.scale
        df_scores_ = self.df_scores_
        r2 = self.df_explained_variance_
        q2_test = self.q2_test
        
        if color_ is not None:
            if color_ not in self.label:
                raise ValueError('color must be in y')
        if symbol is not None:
            if len(symbol) != len(self.label):
                raise ValueError('symbol must have the same number of samples as y')

        if color_ is None:
            color_ = df_scores_['Group']
        else:
            color_ = color_
            




        #check symbol dimension must be equal to y
        if symbol is not None:
            if len(symbol) != len(self.label):
                raise ValueError('symbol must have the same number of samples as y')

        #check symbol_dict must be a dictionary
        if symbol_dict is not None:
            if not isinstance(symbol_dict, dict):
                raise ValueError('symbol_dict must be a dictionary')
        else:
            symbol_dict = None

        #check color_dict must be a dictionary
        if color_dict is not None:
            if not isinstance(color_dict, dict):
                raise ValueError('color_dict must be a dictionary')
        else:
            color_dict = None

        # pc must be a list of 2
        if not isinstance(pc, list):
            raise ValueError("pc must be a list of string \n Example: pc=['PC1', 'PC2']")
        if len(pc) != 2:
            raise ValueError('pc must be a list of 2')
        # pc must be match with columns of df_scores_
        if pc[0] not in self.df_scores_.columns:
            raise ValueError("pc must be in df_scores_ columns \n Example: pc=['PC1', 'PC2']")
        if pc[1] not in self.df_scores_.columns:
            raise ValueError("pc must be in df_scores_ columns \n Example: pc=[\'PC1\', \'PC2\']")

        

        df_scores_ = self.df_scores_
        r2 = self.df_explained_variance_
        q2_test = self.q2_test



        fig = px.scatter(df_scores_, x=pc[0], y=pc[1], color=color_,
                        symbol=symbol, 
                        color_discrete_map=color_dict, 
                        symbol_map=symbol_dict, 
                        title=f'<b>PCA Scores Plot<b> {scale} scaling', 
                        height=fig_height, width=fig_width,
                        labels={'Group': 'Group',
                                pc[0]: "{} R<sup>2</sup>X: {}%".format(pc[0], np.round(r2.loc[r2.loc[r2['PC']==pc[0]].index, 'Explained variance'].values[0]*100, decimals=2)),
                                pc[1]: "{} R<sup>2</sup>X: {}%".format(pc[1], np.round(r2.loc[r2.loc[r2['PC']==pc[1]].index, 'Explained variance'].values[0]*100, decimals=2))},
                        text=text_)

        fig.update_traces(marker=dict(size=marker_size, 
                            opacity=marker_opacity, 
                            line=dict(width=2, color='DarkSlateGrey')))


        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=1.0,
                                y=0.05,
                                showarrow=False,
                                text=f"<b>R<sup>2</sup>X (Cum): {np.round(r2.loc[r2.loc[r2['PC']==pc[1]].index, 'Cumulative variance'].values[0]*100, decimals=2)}%<b>",
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=1.0,
                                y=0.01,
                                showarrow=False,
                                text=f"<b>Q<sup>2</sup>X (Cum): {np.round(q2_test*100, decimals=2)}%<b>",
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_shape(type='path',
                path=confidence_ellipse(df_scores_[pc[0]], df_scores_[pc[1]]))



        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(
            title={
                'y':1,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))
        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')

        return fig



    def plot_loading_(self, pc=['PC1', 'PC2'], height_=600, width_=1800):

        df_loadings_ = self.df_loadings_

        loadings_label = self.features_name


        fig = px.line(df_loadings_, x=loadings_label, y=pc,
                                height=height_, width=width_,
                                title='Loadings plot',
                                text=loadings_label)

        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_layout(title={'y':0.95,
                                'x':0.5,
                                'xanchor': 'center',
                                'yanchor': 'top'},
                        font=dict(size=20))
        
        fig.update_layout(scene={'xaxis': {'autorange': 'reversed'}})
                
        fig.update_traces(marker=dict(size=1))
        fig.update_layout(xaxis_title="𝛿<sub>H</sub> in ppm")
        
        
        return fig






    def plot_pca_trajectory(self,time_, time_order, stat_ = ['mean', 'sem'], pc=['PC1', 'PC2'],
                            color_dict = None, symbol_dict = None, 
                            height_=900, width_=1300,
                            marker_size=35, marker_opacity=0.7, ):


        from .pca_ellipse import confidence_ellipse


        #check time_order must be a dictionary
        if not isinstance(time_order, dict):
            raise ValueError("time_order must be a dictionary \n Example: time_order = {0: 'Day 1', 1: 'Day 2', 2: 'Day 3'}")

        #chack time_order ditionary index must be match with time_.unique()
        if set(time_order.keys()) != set(time_):
            raise ValueError('time_order dictionary index must be match with time_')


        #check time are not missing
        if time_ is None:
            raise ValueError('time_ must be provided fot time trajectory analysis')

        #check time_ must be pandas series, list or array
        if not isinstance(time_, (pd.Series, np.ndarray, list)):
            raise ValueError('time_ must be a series, list or array')
        if len(time_) != len(self.label):
            raise ValueError('time_ must have the same number of samples as group')

        


        df_scores_ = self.df_scores_
        r2 = self.df_explained_variance_
        q2_test = self.q2_test
        df_scores_['Time point'] = time_
        



        #check stat_[0] must be mean or median
        if stat_[0] not in ['mean', 'median']:
            raise ValueError('stat_[0] must be mean or median')
        #check stat_[1] must be sem, std
        if stat_[1] not in ['sem','std']:
            raise ValueError('stat_[1] must be sem or std')

        if stat_[0] == 'mean':
            df_scores_point = df_scores_.groupby(['Group', 'Time point']).mean()
        if stat_[0] == 'median':
            df_scores_point = df_scores_.groupby(['Group', 'Time point']).median()

        if stat_[1] == 'sem':
            err_df = df_scores_.groupby(['Group', 'Time point']).sem()
        if stat_[1] == 'std':
            err_df = df_scores_.groupby(['Group', 'Time point']).std()


        df_scores_point.reset_index(inplace=True)
        df_scores_point['Time point number'] = df_scores_point['Time point'].map(time_order)
        df_scores_point.sort_values(by=['Group', 'Time point number'], inplace=True)

        err_df.reset_index(inplace=True)
        err_df['Time point number'] = err_df['Time point'].map(time_order)
        err_df.sort_values(by=['Group', 'Time point number'], inplace=True)



        #check color_dict must be a dictionary
        if color_dict is not None:
            if not isinstance(color_dict, dict):
                raise ValueError('color_dict must be a dictionary')
        else:
            color_dict = None


        if symbol_dict is not None:
            if not isinstance(symbol_dict, dict):
                raise ValueError('symbol_dict must be a dictionary')
        else:
            symbol_dict = None
        


        #If user not input color_dict then get unique of label and create color_dict
        if color_dict is None:
            color_dict = {i: px.colors.qualitative.Plotly[i] for i in range(len(df_scores_point['Group'].unique()))}

        #Change {0: '#636EFA', 1: '#EF553B', 2: '#00CC96'} to {'Group1': '#636EFA', 'Group2': '#EF553B', 'Group3': '#00CC96'}
        color_dict = {df_scores_point['Group'].unique()[i]: list(color_dict.values())[i] for i in range(len(df_scores_point['Group'].unique()))}
        
        n_group = df_scores_point['Group'].unique()



        fig = px.line(df_scores_point, x=pc[0], y=pc[1], line_group='Time point', 
                        error_x=err_df[pc[0]], error_y=err_df[pc[1]],
                        color='Group', color_discrete_map=color_dict,
                        symbol='Time point', symbol_map=symbol_dict,
                        title='<b>Principle component analysis ({})<b>'.format(self.scale), 
                        height=height_, width=width_,
                        labels={
                            pc[0]: "{} R<sup>2</sup>X: {} %".format(pc[0], np.round(r2.loc[r2.loc[r2['PC']==pc[0]].index, 'Explained variance'].values[0]*100, decimals=2)),
                            pc[1]: "{} R<sup>2</sup>X: {} %".format(pc[1], np.round(r2.loc[r2.loc[r2['PC']==pc[1]].index, 'Explained variance'].values[0]*100, decimals=2)),
                            'Group': 'Group'
                            })


        



        for i in range(len(n_group)):
            # create a new trace for the connecting line
            fig.add_trace(go.Scatter(
                x=df_scores_point.loc[list(df_scores_point.loc[df_scores_point['Group'] == df_scores_point['Group'].unique()[i]].index), pc[0]], # x-coordinates of the line
                y=df_scores_point.loc[list(df_scores_point.loc[df_scores_point['Group'] == df_scores_point['Group'].unique()[i]].index), pc[1]], # y-coordinates of the line
                mode='lines', # specify the trace type as lines
                line=dict(color=color_dict[n_group[i]], width=2), # set the color and width of the line
                showlegend=False # hide the trace from the legend
            ))



        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=1.0,
                                y=0.05,
                                showarrow=False,
                                text=f"<b>R<sup>2</sup>X (Cum): {np.round(r2.loc[r2.loc[r2['PC']==pc[1]].index, 'Cumulative variance'].values[0]*100, decimals=2)}%<b>",
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_annotation(dict(font=dict(color="black",size=20),
                                #x=x_loc,
                                x=1.0,
                                y=0.01,
                                showarrow=False,
                                text=f"<b>Q<sup>2</sup>X (Cum): {np.round(q2_test*100, decimals=2)}%<b>",
                                textangle=0,
                                xref="paper",
                                yref="paper"),
                                # set alignment of text to left side of entry
                                align="left")

        fig.add_shape(type='path',
                path=confidence_ellipse(df_scores_point[pc[0]], df_scores_point[pc[1]]))


                    #update axis as scitifics
        fig.update_xaxes(tickformat=".1e")
        fig.update_yaxes(tickformat=".1e")



        fig.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor='Black')
        fig.update_xaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
        fig.update_layout(
            title={
                'y':0.95,
                'x':0.5,
                'xanchor': 'center',
                'yanchor': 'top'},
            font=dict(size=20))


        fig.update_traces(marker=dict(size=marker_size, opacity=marker_opacity, line=dict(width=2, color='DarkSlateGrey')))

        fig.update_layout(paper_bgcolor='rgba(0,0,0,0)',plot_bgcolor='rgba(0,0,0,0)')
        
        
        return fig