Perform volcano plot analysis

Perform volcano plot analysis using data for named metabolites from the metabolomics workbench or uploaded data files.

Note: This notebook contains IPython widgets. Consequently, you won't be able to use Kernal/Restart & Restart command to automatically execute all cells in the notebook. You must use Run command individually to execute each cell and advance to the next cell.

Import Python modules...

In [ ]:
from __future__ import print_function

import os
import sys
import time
import re

import requests

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

import scipy
import statsmodels.stats.multitest

import ipywidgets as widgets

from IPython.display import display, HTML
from IPython import __version__ as ipyVersion

# Import MW modules from the current directory or default Python directory...
import MWUtil

%matplotlib inline

print("Python: %s.%s.%s" % sys.version_info[:3])
print("IPython: %s" % ipyVersion)
print("ipywidgets: %s" % widgets.__version__)

print()
print(time.asctime())

The URL PATH

The MW REST URL consists of three main parts, separated by forward slashes, after the common prefix specifying the invariant base URL (https://www.metabolomicsworkbench.org/rest/):

https://www.metabolomicsworkbench.org/rest/context/input_specification/output_specification

Part 1: The context determines the type of data to be accessed from the Metabolomics Workbench, such as metadata or results related to the submitted studies, data from metabolites, genes/proteins and analytical chemistry databases as well as other services related to mass spectrometry and metabolite identification:

context = study | compound | refmet | gene | protein | moverz | exactmass

Part 2: The input specification consists of two required parameters describing the REST request:

input_specification = input_item/input_value

Part 3: The output specification consists of two parameters describing the output generated by the REST request:

output_specification = output_item/(output_format)

The first parameter is required in most cases. The second parameter is optional. The input and output specifications are context sensitive. The context determines the values allowed for the remaining parameters in the input and output specifications as detailed in the sections below.

Setup MW REST base URL...

In [ ]:
MWBaseURL = "https://www.metabolomicsworkbench.org/rest"

Retrieve or upload data for named metabolites...

In [ ]:
# Initialize data...
StudiesResultsData = None
RetrievedMWData = None
In [ ]:
# Set up UIF info text...
TopInfoTextHTML = widgets.HTML(value = "<strong>Retrieve or upload data and process any missing values</strong>", 
                               placeholder='', description='')

# Setup UIF to process any missing values...
MissingValuesMethods = ["NoAction", "DeleteRows", "DeleteColumns", "ReplaceByColumnMean", "ReplaceColumnMedian", "ReplaceByZero" , "LinearInterpolation"]
MissingValuesMethodsDropdown = widgets.Dropdown(options = MissingValuesMethods,
                                                value = "NoAction",
                                                description = " ")
ProcessMissingValueTopTextHTML = widgets.HTML(value = "Method for processing missing values:", 
                                              placeholder='', description='')

# Setup UIF to retrieve...
StudyIDText = widgets.Text(value = "ST000001 ST000002", description = "Study ID (s)",
                           placeholder = "Type study ID", disabled = False,
                           layout = widgets.Layout(margin='0 10px 0 0'))
RetrieveDataBtn = widgets.Button(description = 'Retrieve Data', disabled = False, button_stype = '',
                                 tooltip = "Retrieve data for study ID")

RetrieveDataOutput = widgets.Output()
def RetrieveDataBtnEventHandler(Object):
    global StudiesResultsData, RetrievedMWData
    
    RetrievedMWData = True
    StudiesResultsData = None
    
    StudyIDs = StudyIDText.value
    MissingValuesMethod = MissingValuesMethodsDropdown.value
    
    RetrieveDataOutput.clear_output()
    UploadDataOutput.clear_output()
    with RetrieveDataOutput:
        if len(StudyIDs):
            print("\nProcessing study ID(s): %s" % StudyIDs)
            StudiesResultsData = MWUtil.RetrieveStudiesAnalysisAndResultsData(StudyIDs, MWBaseURL, MissingValuesMethod)
            DisplayData = False if len(StudiesResultsData.keys()) > 5 else True
            MWUtil.ListStudiesAnalysisAndResultsData(StudiesResultsData, DisplayDataFrame = DisplayData,
                                              IPythonDisplayFuncRef = display, IPythonHTMLFuncRef = HTML)
        else:
            print("\nNo study ID(s) specified...")

RetrieveDataBtn.on_click(RetrieveDataBtnEventHandler)

# Setup UIF to upload data file(s)...
FileUploadBtn = widgets.FileUpload(description = 'Upload File(s)', accept='.csv,.txt,.tsv', multiple = True,
                                  disabled = False)
FileUploadTextHTML = widgets.HTML(value = "<strong>File format:</strong> Col 1: Sample names; \
                                Col 2: Class identifiers; Remaining cols: Named metabolites; \
                                <strong>Exts: </strong>.csv, .txt, or .tsv", placeholder='', description='')

UploadDataOutput = widgets.Output()
def FileUploadBtnEventHandler(Change):
    global StudiesResultsData, RetrievedMWData
    
    RetrievedMWData = False
    StudiesResultsData = None
    
    MissingValuesMethod = MissingValuesMethodsDropdown.value
    UploadedDataInfo = FileUploadBtn.value
    
    RetrieveDataOutput.clear_output()
    UploadDataOutput.clear_output()
    with UploadDataOutput:
        StudiesResultsData = MWUtil.RetrieveUploadedData(UploadedDataInfo, MissingValuesMethod)
        DisplayData = False if len(StudiesResultsData.keys()) > 5 else True
        MWUtil.ListStudiesAnalysisAndResultsData(StudiesResultsData, DisplayDataFrame = DisplayData,
                                                IPythonDisplayFuncRef = display, IPythonHTMLFuncRef = HTML)
        
FileUploadBtn.observe(FileUploadBtnEventHandler, names = 'value')

# Setup UIF to retrieve or upload data file...

DataWarningTextHTML = widgets.HTML(value = "<div class='alert alert-warning'><strong>Warning:</strong> Don't re-run the current cell after specifying study ID(s) or selecting file(s) and retrieving the data. Click on the next cell to advance.</div>", placeholder='', description='')
OrTextHTML = widgets.HTML(value = "<strong>Or</strong>", placeholder='', description='')

UIFDataBoxes = []
UIFDataBoxes.append(widgets.HBox([TopInfoTextHTML]))
UIFDataBoxes.append(widgets.HBox([ProcessMissingValueTopTextHTML, MissingValuesMethodsDropdown]))
UIFDataBoxes.append(widgets.HBox([StudyIDText, RetrieveDataBtn],
                                 layout = widgets.Layout(margin='10px 0 0 0')))
UIFDataBoxes.append(widgets.HBox([OrTextHTML]))
UIFDataBoxes.append(widgets.HBox([FileUploadBtn]))
UIFDataBoxes.append(widgets.HBox([FileUploadTextHTML]))
UIFDataBoxes.append(widgets.HBox([DataWarningTextHTML]))

for UIFDataBox in UIFDataBoxes:
    display(UIFDataBox)

display(RetrieveDataOutput)
display(UploadDataOutput)
In [ ]:
MWUtil.CheckAndWarnEmptyStudiesData(StudiesResultsData, RetrievedMWData, StudyIDText.value)

Setup UIF for selecting and plotting available data...

In [ ]:
# Setup UIF data...
StudiesUIFData = MWUtil.SetupUIFDataForStudiesAnalysisAndResults(StudiesResultsData, MinClassCount = 2)
In [ ]:
MWUtil.CheckAndWarnEmptyStudiesUIFData(StudiesUIFData, RetrievedMWData, StudyIDText.value)
In [ ]:
# Setup a function to generate data for volcano plots...
def GenerateVolcanoPlotData(DataFrame, FirstClassNum, SecondClassNum, ClassNumColID = "ClassNum", ContainsClassCol = True):
    """Generate data for volcano plots."""
    
    VolcanoPlotDataFrame = None
    ErrMsg = None
    
    with np.errstate(all = 'raise'):
        try:
            VolcanoPlotDataFrame = _GenerateVolcanoPlotData(DataFrame, FirstClassNum, SecondClassNum, ClassNumColID, ContainsClassCol)
        except FloatingPointError as NPErrMsg:
            ErrMsg = "Failed to generate data for volcano plot: %s" % NPErrMsg
    
    return (VolcanoPlotDataFrame, ErrMsg)
    
def _GenerateVolcanoPlotData(DataFrame, FirstClassNum, SecondClassNum, ClassNumColID = "ClassNum", ContainsClassCol = True):
    """Generate data for volcano plots."""
    
    # Drop Class column...
    if ContainsClassCol:
        DataFrame = DataFrame.drop('Class', axis = 1)
        
    # Extract data for two specified classes...
    DataA = DataFrame[DataFrame[ClassNumColID] == FirstClassNum]
    DataB = DataFrame[DataFrame[ClassNumColID] == SecondClassNum]
    
    # Drop ClassNum columns from extracted data...
    DataA = DataA.drop(ClassNumColID, axis = 1)
    DataB = DataB.drop(ClassNumColID, axis = 1)
    
    # Tranform data...
    print("Transforming data...")
    DataA = np.log2(DataA)
    DataB = np.log2(DataB)
    
    # Calculate statistics...
    print("Calculating statistics")
    TStatistics, PValues = scipy.stats.ttest_ind(DataA, DataB, equal_var = False, nan_policy = 'omit')
    TStatistics = TStatistics.tolist()
    PValues = PValues.tolist()
    
    # Adjust P-values..
    print("Adjusting P-values...")
    Rejects, AdjustedPValues = statsmodels.stats.multitest.fdrcorrection(PValues, alpha=0.05, method='indep', is_sorted=False)
    AdjustedPValues = AdjustedPValues.tolist()
    
    # Calculate fold change...
    MeanA = DataA.mean(axis = 0)
    MeanB = DataB.mean(axis = 0)
    Log2FoldChange = MeanB.subtract(MeanA)
    Log2FoldChange = Log2FoldChange.tolist()
    
    # Cast np array to a list...
    Log10PValues = -np.log10(PValues)
    Log10PValues = Log10PValues.tolist()
    
    VolcanoPlotDataFrame = pd.DataFrame(
        [Log2FoldChange, PValues, Log10PValues, AdjustedPValues, TStatistics],
        columns = DataA.columns,
        index=['log2(FoldChange)', 'P-value', '-log10(P-value)', 'AdjustedP-value', 't-Statistic']).transpose()
    
    return VolcanoPlotDataFrame
    
def ListSignificantMetabolitesByVolcanoPlotData(StudyID, AnalysisID, VolcanoPlotDataFrame,
                                                LogFoldChangeColID ="log2(FoldChange)",
                                                PValueColID = "P-value", 
                                                LogFoldChangeThreshold = 1.0, 
                                                PValueThreshold = 0.05):
    """List significant metabolites."""
    
    SignificantMetabolitesDataFrame = VolcanoPlotDataFrame[VolcanoPlotDataFrame[PValueColID] <= PValueThreshold]
    SignificantMetabolitesDataFrame = SignificantMetabolitesDataFrame[np.abs(SignificantMetabolitesDataFrame[LogFoldChangeColID]) >= LogFoldChangeThreshold]
    
    # Sort by log2(FoldChange) in ascending order...
    SignificantMetabolitesDataFrame = SignificantMetabolitesDataFrame.sort_values(by = [LogFoldChangeColID], ascending = True)
    
    print("\nMetabolites changes at P-value <= %s and abs(log2(FoldChange)) > %s:" % (PValueThreshold, LogFoldChangeThreshold))
    
    if RetrievedMWData:
        FileName = "%s_%s_Significant_Changes_Data.csv" % (StudyID, AnalysisID)
    else:
        FileRoot, FileExt = os.path.splitext(StudyID)
        FileName = "%s_Significant_Changes_Data.csv" % (FileRoot)
        
    HTMLText = MWUtil.SetupCSVDownloadLink(SignificantMetabolitesDataFrame, Title = "Download foldchange data", CSVFilename = FileName)
    display(HTML(HTMLText))
        
    display(HTML(SignificantMetabolitesDataFrame.to_html()))
    
# Setup a function to draw volcano plot...
def DrawVolcanoPlot(VolcanoPlotDataFrame, LogFoldChangeColID ="log2(FoldChange)", PValueColID = "P-value", 
                    LogPValueColID ="-log10(P-value)", LogFoldChangeThreshold = 1.0, PValueThreshold = 0.05,
                    PlotStyle = "darkgrid", FontScale = 1.3, TitleFontWeight = "bold", LabelsFontWeight = "bold",
                    PlotWidth = 9, PlotHeight = 6):
    
    # Setup color for data points...
    DataFrame = VolcanoPlotDataFrame
    
    # Significant up foldchange...
    ColorColID = 'Color'
    DataFrame.loc[(DataFrame[LogFoldChangeColID] >= LogFoldChangeThreshold) 
                  & (DataFrame[PValueColID] <= PValueThreshold), ColorColID] = "red"
    # Significant down foldchange...
    DataFrame.loc[(DataFrame[LogFoldChangeColID] <= -LogFoldChangeThreshold) 
                  & (DataFrame[PValueColID] <= PValueThreshold), ColorColID] = "blue"
    # Significant change...
    DataFrame.loc[(DataFrame[LogFoldChangeColID] >= -LogFoldChangeThreshold)
                  & (DataFrame[LogFoldChangeColID] <= LogFoldChangeThreshold)
                  & (DataFrame[PValueColID] <= PValueThreshold), ColorColID] = "orange"
    # Intermediate change...
    DataFrame[ColorColID].fillna('purple', inplace = True)
    
    ColorsPalette = {"red" : "red", "blue" : "blue", "orange" : "orange", "purple" : "purple"}
    
    sns.set(rc = {'figure.figsize':(PlotWidth, PlotHeight)})
    sns.set(style = PlotStyle, font_scale = FontScale)
    
    Axis = sns.scatterplot(x = LogFoldChangeColID, y = LogPValueColID, hue = ColorColID, data = VolcanoPlotDataFrame,
                           palette = ColorsPalette, legend = False)

    # Draw vertical lines at LogFoldChangeThreshold...
    Axis.axvline(-LogFoldChangeThreshold, color = 'violet', linewidth = 1, linestyle = "dashed")
    Axis.axvline(LogFoldChangeThreshold, color = 'violet', linewidth = 1, linestyle = "dashed")
    
    # Draw a horizontal line at -log10(PValueThreshold)
    HLinePos = -np.log10(PValueThreshold)
    Axis.axhline(HLinePos, color = 'green', linewidth = 1, linestyle = "dashed")
    
    # Set title and labels...
    Axis.set_title("Volcano Plot", fontweight = TitleFontWeight)
    Axis.set_xlabel(LogFoldChangeColID, fontweight = LabelsFontWeight)
    Axis.set_ylabel(LogPValueColID , fontweight = LabelsFontWeight)
    
    plt.show()
    
    # Drop color column...
    DataFrame.drop(ColorColID, axis = 1, inplace = True)
    
In [ ]:
# Setup UIF...
FirstStudyID = StudiesUIFData["StudyIDs"][0]
StudiesDropdown = widgets.Dropdown(options = StudiesUIFData["StudyIDs"], value = FirstStudyID, 
                                   description="Study:", disabled = False)

FirstAnalysisID = StudiesUIFData["AnalysisIDs"][FirstStudyID][0]
AnalysisDropdown = widgets.Dropdown(options = StudiesUIFData["AnalysisIDs"][FirstStudyID], value = FirstAnalysisID,
                                    description = "Analysis:", disabled = False)

FirstClassID = StudiesUIFData["ClassIDs"][FirstStudyID][FirstAnalysisID][0]
FirstClassDropdown = widgets.Dropdown(options = StudiesUIFData["ClassIDs"][FirstStudyID][FirstAnalysisID],
                                      value = FirstClassID, description = "Class A:", disabled = False)

SecondClassID = StudiesUIFData["ClassIDs"][FirstStudyID][FirstAnalysisID][1]
SecondClassDropdown = widgets.Dropdown(options = StudiesUIFData["ClassIDs"][FirstStudyID][FirstAnalysisID],
                                       value = SecondClassID, description = "Class B:", disabled = False)

FirstClassNum = StudiesResultsData[FirstStudyID][FirstAnalysisID]["class_names_to_nums"][FirstClassID]
SecondClassNum = StudiesResultsData[FirstStudyID][FirstAnalysisID]["class_names_to_nums"][SecondClassID]

VolcanoPlotDataFrame, ErrMsg = GenerateVolcanoPlotData(StudiesResultsData[FirstStudyID][FirstAnalysisID]["data_frame"],
                                                       FirstClassNum, SecondClassNum,
                                                       ClassNumColID = "ClassNum", ContainsClassCol = True)

def GetMaxPValue(VolcanoPlotDataFrame):
    return "%.2f" % np.max(VolcanoPlotDataFrame[PValueColID]) if VolcanoPlotDataFrame is not None else DefaultMaxPValue

def GetMaxLogFoldChange(VolcanoPlotDataFrame):
    return "%.1f" % np.max(VolcanoPlotDataFrame[LogFoldChangeColID]) if VolcanoPlotDataFrame is not None else DefaultMaxLogFoldChange


PValueColID = "P-value"
DefaultPValue = "0.05"
DefaultMaxPValue = "5.00"
MinPValue = "0.01"
MaxPValue = GetMaxPValue(VolcanoPlotDataFrame)
PValueSlider = widgets.FloatSlider(value = DefaultPValue, min = MinPValue, max = MaxPValue, step = 0.01,
                                   description = 'P-value:', disabled = False,
                                   continuous_update = False, orientation = 'horizontal',
                                   readout = True, readout_format = '.2f')

LogFoldChangeColID = "log2(FoldChange)"
DefaultLogFoldChangeValue = "1.0"
DefaultMaxLogFoldChange = "5.0"
MinLogFoldChange = "0.1"
MaxLogFoldChange = GetMaxLogFoldChange(VolcanoPlotDataFrame)
LogFoldChangeSlider = widgets.FloatSlider(value = DefaultLogFoldChangeValue, min = MinLogFoldChange, max = MaxLogFoldChange,
                                          step = 0.1, description = 'log2(FoldChange):',  disabled = False,
                                          continuous_update = False, orientation = 'horizontal',
                                          readout_format = '.1f')

PlotStyles = ["Darkgrid", "Whitegrid", "Dark", "White", "Ticks"]
PlotStylesDropdown = widgets.Dropdown(options = PlotStyles, value = "Darkgrid", description = "Plot style:")

DefaultPlotWidth = 9
DefaultPlotHeight = 6

PlotSizeText = widgets.Text(value = "9x6", description = "Plot size:", placeholder = "Type WxH; Hit enter",
                            disabled = False, continuous_update=False)

DataLayout = widgets.Layout(margin='0 0 4px 0')
StudiesDataHBox = widgets.HBox([StudiesDropdown, AnalysisDropdown], layout = DataLayout)
ClassesDataHBox = widgets.HBox([FirstClassDropdown, SecondClassDropdown], layout = DataLayout)
SlidersDataHBox = widgets.HBox([PValueSlider, LogFoldChangeSlider], layout = DataLayout)
PlotsDataHBox = widgets.HBox([PlotStylesDropdown, PlotSizeText], layout = DataLayout)

Output = widgets.Output()
OutputPlot = widgets.Output()

UpdatePlot = True

def DisablePlotUpdate():
    global UpdatePlot
    UpdatePlot = False

def EnablePlotUpdate():
    global UpdatePlot
    UpdatePlot = True

def GetUpdatePlotStatus():
    global UpdatePlot
    return True if UpdatePlot else False

# Setup function to update dropdown options...
def UpdateAnalysisDropdown(StudyID):
    AnalysisDropdown.options = StudiesUIFData["AnalysisIDs"][StudyID]
    AnalysisDropdown.value = StudiesUIFData["AnalysisIDs"][StudyID][0]
    
def UpdateFirstClassDropdown(StudyID, AnalysisID):
    FirstClassDropdown.options = StudiesUIFData["ClassIDs"][StudyID][AnalysisID]
    FirstClassDropdown.value = StudiesUIFData["ClassIDs"][StudyID][AnalysisID][0]

def UpdateSecondClassDropdown(StudyID, AnalysisID):
    SecondClassDropdown.options = StudiesUIFData["ClassIDs"][StudyID][AnalysisID]
    SecondClassDropdown.value = StudiesUIFData["ClassIDs"][StudyID][AnalysisID][1]

def UpdateSliders(StudyID, AnalysisID):
    FirstClassID = FirstClassDropdown.value
    SecondClassID = SecondClassDropdown.value
    
    FirstClassNum = StudiesResultsData[StudyID][AnalysisID]["class_names_to_nums"][FirstClassID]
    SecondClassNum = StudiesResultsData[StudyID][AnalysisID]["class_names_to_nums"][SecondClassID]
    
    VolcanoPlotDataFrame, ErrMsg = GenerateVolcanoPlotData(StudiesResultsData[StudyID][AnalysisID]["data_frame"],
                                                           FirstClassNum, SecondClassNum,
                                                           ClassNumColID = "ClassNum", ContainsClassCol = True)

    MinPValue = "0.01"
    MaxPValue = GetMaxPValue(VolcanoPlotDataFrame)
    PValueSlider.value = DefaultPValue
    PValueSlider.min = MinPValue
    PValueSlider.max = MaxPValue

    MinLogFoldChange = "0.1"
    MaxLogFoldChange = GetMaxLogFoldChange(VolcanoPlotDataFrame)
    LogFoldChangeSlider.value = DefaultLogFoldChangeValue
    LogFoldChangeSlider.min = MinLogFoldChange
    LogFoldChangeSlider.max = MaxLogFoldChange 
    
# Setup dropdown event handlers...
def StudiesDropdownEventHandler(Change):
    StudyID = Change["new"]
    
    DisablePlotUpdate()
    UpdateAnalysisDropdown(StudyID)
    UpdateFirstClassDropdown(StudyID, StudiesUIFData["AnalysisIDs"][StudyID][0])
    UpdateSecondClassDropdown(StudyID, StudiesUIFData["AnalysisIDs"][StudyID][0])
    
    EnablePlotUpdate()
    
    PlotData()

def AnalysisDropdownEventHandler(Change):
    UpdatePlotStatus = GetUpdatePlotStatus()
    if UpdatePlotStatus:
        DisablePlotUpdate()
    
    UpdateFirstClassDropdown(StudiesDropdown.value, Change["new"])
    UpdateSecondClassDropdown(StudiesDropdown.value, Change["new"])
    
    UpdateSliders(StudiesDropdown.value, Change["new"])
    
    if UpdatePlotStatus:
        EnablePlotUpdate()
    
    PlotData()

def FirstClassDropdownEventHandler(Change):
    PlotData()

def SecondClassDropdownEventHandler(Change):
    PlotData()

def PValueSliderEventHandler(Change):
    PlotData()

def LogFoldChangeSliderEventHandler(Change):
    PlotData()
    
def PlotStylesDropdownEventHandler(Change):
    PlotData()
    
def PlotSizeTextEventHandler(Change):
    PlotData()
    
# Bind required event handlers...
StudiesDropdown.observe(StudiesDropdownEventHandler, names = 'value')
AnalysisDropdown.observe(AnalysisDropdownEventHandler, names = 'value')
FirstClassDropdown.observe(FirstClassDropdownEventHandler, names = 'value')

SecondClassDropdown.observe(SecondClassDropdownEventHandler, names = 'value')
FirstClassDropdown.observe(FirstClassDropdownEventHandler, names = 'value')

PValueSlider.observe(PValueSliderEventHandler, names='value')
LogFoldChangeSlider.observe(LogFoldChangeSliderEventHandler, names='value')

PlotStylesDropdown.observe(PlotStylesDropdownEventHandler, names = 'value')
PlotSizeText.observe(PlotSizeTextEventHandler, names = 'value')
    
# Set up function to generate volcano plot...
def PlotData():
    if not UpdatePlot:
        return
    
    Output.clear_output()
    OutputPlot.clear_output()
    
    StudyID = StudiesDropdown.value
    AnalysisID = AnalysisDropdown.value
    FirstClassID = FirstClassDropdown.value
    SecondClassID = SecondClassDropdown.value
    DataFrame = StudiesResultsData[StudyID][AnalysisID]["data_frame"]
    
    PValue = PValueSlider.value
    LogFoldChange = LogFoldChangeSlider.value
    
    # First and second class IDs must be different...
    FirstClassNum = StudiesResultsData[StudyID][AnalysisID]["class_names_to_nums"][FirstClassID]
    SecondClassNum = StudiesResultsData[StudyID][AnalysisID]["class_names_to_nums"][SecondClassID]
    if FirstClassNum == SecondClassNum:
        with Output:
            print("Selected classes must be different\nClassNum: %s; Class Name: %s\n" % (FirstClassNum, FirstClassID))
            ListClassInformation(StudyID, AnalysisID)
            return
    
    Style = PlotStylesDropdown.value
    Style = Style.lower()
    
    PlotSize = PlotSizeText.value.lower()
    PlotSize = re.sub(" ", "", PlotSize)
    PlotSizeWords = PlotSize.split("x")
    
    if len(PlotSizeWords) == 2 and len(PlotSizeWords[0]) > 0 and len(PlotSizeWords[1]) > 0:
        Width = float(PlotSizeWords[0])
        Height = float(PlotSizeWords[1])
    else:
        Width = DefaultPlotWidth
        Height = DefaultHeight
        with Output:
            print("Invalid plot size; Using default plot size: %sx%s\n" % (Width, Height))
    
    with OutputPlot:
        # Retrieve data for volcano plot...
        VolcanoPlotDataFrame, ErrMsg = GenerateVolcanoPlotData(DataFrame, FirstClassNum, SecondClassNum,
                                                               ClassNumColID = "ClassNum", ContainsClassCol = True)
        if VolcanoPlotDataFrame is None:
            if ErrMsg is not None:
                print("%s" % ErrMsg)
            print("\nTry data for another analysis or study...")
            FileName = "%s_%s_Data.csv" % (StudyID, AnalysisID)
            HTMLText = MWUtil.SetupCSVDownloadLink(DataFrame, Title = "Download and review data", CSVFilename = FileName)
            display(HTML(HTMLText))
            return
        
        # Generate volcano plot...
        DrawVolcanoPlot(VolcanoPlotDataFrame, PlotStyle = Style, PlotWidth = Width, PlotHeight = Height,
                        PValueThreshold = PValue, LogFoldChangeThreshold = LogFoldChange)
    
    with Output:
        print("Selected classes:")
        print("Class A - ClassNum: %s; ClassName: %s" % (FirstClassNum, FirstClassID))
        print("Class B - ClassNum: %s; ClassName: %s" % (SecondClassNum, SecondClassID))
        print("\n")
        
        MWUtil.ListClassInformation(StudiesResultsData, StudyID, AnalysisID, RetrievedMWData)
        
        if RetrievedMWData:
            FileName = "%s_%s_Data.csv" % (StudyID, AnalysisID)
            HTMLText = MWUtil.SetupCSVDownloadLink(DataFrame, Title = "Download data", CSVFilename = FileName)
            display(HTML(HTMLText))
        
        ListSignificantMetabolitesByVolcanoPlotData(StudyID, AnalysisID, VolcanoPlotDataFrame, PValueThreshold = PValue,
                                                    LogFoldChangeThreshold = LogFoldChange)


display(StudiesDataHBox)
display(ClassesDataHBox)
display(SlidersDataHBox)
display(PlotsDataHBox)

display(OutputPlot)
display(Output)

PlotData()