"""
202012

General purpose plotting from arbitrary csv files

Started as specifically for SanPy (using reanalyze.py) from a database of spikes

20210112, extending it to any csv for use with bImPy nodes/edges
    See: bImPy/bimpy/analysis/bScrapeNodesAndEdges.py

I want this to run independent of SanPy, to do this:
    copy (pandas model, checkbox delegate) from sanpy.butil
    into a local folder and change imports

Requires (need to make local copies of (pandas model, checkbox delegate)
    pandas
    numpy
    PyQt5
    matplotlib
    seaborn
    #mplcursors
    openpyxl # to load xlsx
"""

import os, sys, io, csv
from collections import OrderedDict
import traceback
import pandas as pd
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT
from matplotlib.figure import Figure
import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt  # abb 202012 added to set theme
import seaborn as sns
# import mplcursors  # popup on hover

# sysPath = os.path.dirname(sys.path[0])
# sysPath = os.path.join(sysPath, 'sanpy')
# print('bScatterPlotWidget2.py sysPath:', sysPath)
# sys.path.append(sysPath)

# print('os.getcwd():', os.getcwd())
# import sanpy
# import bUtil # from sanpy folder, for pandas model


def old_loadDatabase(path):
    """
    path: full path to .csv file generated with reanalyze.py
    """
    # path = '/Users/cudmore/data/laura-ephys/Superior vs Inferior database_master.csv'
    masterDf = None
    if not os.path.isfile(path):
        print("eror: bUtil.loadDatabase() did not find file:", path)
    elif path.endswith(".csv"):
        masterDf = pd.read_csv(path, header=0)  # , dtype={'ABF File': str})
    elif path.endswith(".xls"):
        masterDf = pd.read_excel(path, header=0)  # , dtype={'ABF File': str})
    elif path.endswith(".xlsx"):
        masterDf = pd.read_excel(
            path, header=0, engine="openpyxl"
        )  # , dtype={'ABF File': str})
    else:
        print("error: file type not supported. Expecting csv/xls/xlsx. Path:", path)

    # self.masterDfColumns = self.masterDf.columns.to_list()

    # not sure what this was for ???
    # 20210112, put back in if necc
    # self.masterCatColumns = ['Condition', 'File Number', 'Sex', 'Region', 'filename', 'analysisname']
    # self.masterCatColumns = self.categoricalList

    # print(self.masterDf.head())
    """
    print(masterDf.info())
    print('masterDf.iloc[0,3]:', masterDf.iloc[0,3], type(masterDf.iloc[0,3]))
    print('start seconds:', masterDf['Start Seconds'].dtype.type)
    print('start seconds:', masterDf['Start Seconds'].dtype)
    """
    #
    return masterDf


def printDict(d, withType=False):
    for k, v in d.items():
        if withType:
            print(f"  {k}: {v} {type(v)}")
        else:
            print(f"  {k}: {v}")


class myPandasModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        """
        data: pandas dataframe
        """
        QtCore.QAbstractTableModel.__init__(self)
        self.verbose = False
        self._data = data
        columnList = self._data.columns.values.tolist()
        if "include" in columnList:
            self.includeCol = columnList.index("include")
        else:
            self.includeCol = None
        # print('pandasModel.__init__() self.includeCol:', self.includeCol)
        self.columns_boolean = ["include"]

    def rowCount(self, parent=None):
        # if self.verbose: print('myPandasModel.rowCount()')
        return self._data.shape[0]

    def columnCount(self, parnet=None):
        # if self.verbose: print('myPandasModel.columnCount()')
        return self._data.shape[1]

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if self.verbose:
            print("myPandasModel.data()")
        if index.isValid():
            if role == QtCore.Qt.DisplayRole:
                # return QtCore.QVariant()

                # return str(self._data.iloc[index.row(), index.column()])
                retVal = self._data.iloc[index.row(), index.column()]
                if isinstance(retVal, np.float64):
                    retVal = float(retVal)
                    retVal = round(retVal, 4)  # round everything to 4 decimal places
                    if np.isnan(retVal):
                        retVal = ""
                elif isinstance(retVal, np.int64):
                    retVal = int(retVal)
                #
                return retVal
            elif role == QtCore.Qt.BackgroundRole:
                # return
                return QtCore.QVariant()

        return None

    def update(self, dataIn):
        if self.verbose:
            print("myPandasModel.update()")

    def setData(self, index, value, role=QtCore.Qt.DisplayRole):
        """
        This is curently limited to only handle checkbox
        todo: extend to allow editing

        Returns:
            True if value is changed. Calls layoutChanged after update.
            False if value is not different from original value.
        """
        if self.verbose:
            print("myPandasModel.setData()")
        print(
            "  myPandasModel.setData() row:",
            index.row(),
            "column:",
            index.column(),
            "value:",
            value,
            type(value),
        )
        # if index.column() == self.includeCol:

        # dataChanged is inherited from QAbstractItemModel
        # topLeftIndex = index
        # bottomRightIndex = index
        # self.dataChanged.emit(index, index)

        if 1:
            # print('value:', value, type(value))
            v = self._data.iloc[index.row(), index.column()]
            # print('before v:',v, type(v))
            # print('isinstance:', isinstance(v, np.float64))
            if isinstance(v, np.float64):
                try:
                    value = float(value)
                except ValueError as e:
                    print("please enter a number")
                    return False

            # set
            self._data.iloc[index.row(), index.column()] = value

            v = self._data.iloc[index.row(), index.column()]
            print("    after v:", v, type(v))
            return True
        return True

    def flags(self, index):
        if self.verbose:
            print("myPandasModel.flags()")
            print("  index.column():", index.column())
        if 1:
            # turn on editing (limited to checkbox for now)
            if index.column() in self.columns_boolean:
                # print('  return with columns_boolean')
                return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable
            # print('  return with ...')
            return (
                QtCore.Qt.ItemIsEditable
                | QtCore.Qt.ItemIsEnabled
                | QtCore.Qt.ItemIsSelectable
            )
        else:
            return QtCore.Qt.ItemIsEnabled

    def headerData(self, col, orientation, role):
        # if self.verbose: print('myPandasModel.headerData()')
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return self._data.columns[col]
        return None


# see: https://stackoverflow.com/questions/17748546/pyqt-column-of-checkboxes-in-a-qtableview
class myCheckBoxDelegate(QtWidgets.QItemDelegate):
    """
    A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
    """

    def __init__(self, parent):
        QtWidgets.QItemDelegate.__init__(self, parent)
        self.verbose = False
        if self.verbose:
            print("myCheckBoxDelegate.__init__()")

    def createEditor(self, parent, option, index):
        """
        Important, otherwise an editor is created if the user clicks in this cell.
        """
        if self.verbose:
            print("myCheckBoxDelegate.createEditor()")
        return None

    def paint(self, painter, option, index):
        """
        Paint a checkbox without the label.

        option: PyQt5.QtWidgets.QStyleOptionViewItem
        index: PyQt5.QtCore.QModelIndex
        """
        if self.verbose:
            print("myCheckBoxDelegate.paint()")
        # print('  option:', option, 'index:', index)
        # print('  index.data():', type(index.data()), index.data())
        # HasCheckIndicator = QtWidget.QStyleOptionViewItem.HasCheckIndicator
        # options.HasCheckIndicator returns hex 4, value of enum
        # how do i query it?
        # print('  ', option.ViewItemFeatures().HasCheckIndicator) # returns PyQt5.QtWidgets.QStyleOptionViewItem.ViewItemFeature
        # print('  ', option.features)
        # print('  ', index.data(QtCore.Qt.CheckStateRole)  )
        # state = index.data(QtCore.Qt.CheckStateRole)
        # print('  state:', state, 'option.HasCheckIndicator:', option.HasCheckIndicator)
        self.drawCheck(
            painter,
            option,
            option.rect,
            QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked,
        )

    def editorEvent(self, event, model, option, index):
        """
        Change the data in the model and the state of the checkbox
        if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
        """
        if self.verbose:
            print("myCheckBoxDelegate.editorEvent()")
        if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
            return False

        if (
            event.type() == QtCore.QEvent.MouseButtonRelease
            and event.button() == QtCore.Qt.LeftButton
        ):
            # Change the checkbox-state
            self.setModelData(None, model, index)
            return True

        return False

    def setModelData(self, editor, model, index):
        """
        The user wanted to change the old state in the opposite.
        """
        if self.verbose:
            print("myCheckBoxDelegate.setModelData()")
            print("  editor:", editor)
            print("  model:", model)
            print("  index:", index)
            print("  index.data():", type(index.data()), index.data())
        # data = index.data()
        # if isinstance(data, str):
        #    return
        newValue = 1 if int(index.data()) == 0 else 0
        model.setData(index, newValue, QtCore.Qt.EditRole)


# class MainWindow(QtWidgets.QWidget):
myInterfaceDefaults = OrderedDict(
    {
        "X Statistic": None,
        "Y Statistic": None,
        "Hue": None,
        "Group By": None,
    }
)


class myTableView(QtWidgets.QTableView):
    def __init__(self, dataType, parent=None):
        """
        dataType: in ['All Spikes', 'File Mean']
        """
        super(myTableView, self).__init__(parent)

        self.dataType = dataType

        self.doIncludeCheckbox = False  # todo: turn this on
        self.keepCheckBoxDelegate = myCheckBoxDelegate(None)

        # self.setFont(QtGui.QFont('Arial', 10))
        self.setSizePolicy(
            QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
        )
        self.setSelectionBehavior(QtWidgets.QTableView.SelectRows)

        p = self.palette()
        color1 = QtGui.QColor("#dddddd")
        color2 = QtGui.QColor("#ffffff")
        p.setColor(QtGui.QPalette.Base, color1)
        p.setColor(QtGui.QPalette.AlternateBase, color2)
        self.setAlternatingRowColors(True)
        self.setPalette(p)

    def slotSelectRow(self, selectDict):
        """
        only selecct if selectDict['dataType'] matches what we are showing
        """
        if self.dataType != selectDict["dataType"]:
            return

        print("slotSelectRow() selectDict:", selectDict)
        ind = selectDict["index"]
        """
        plotDf = selectDict['plotDf']
        index = plotDf.at[ind, 'index']
        index = int(index)
        """
        index = ind
        index -= 1  # !!! MY VISUAL INDEX IN TABLE IS ONE BASED !!!
        column = 0
        modelIndex = self.model().index(index, column)
        self.setCurrentIndex(modelIndex)

    def slotSwitchTableDf(self, newDf):
        """
        set model from df
        """
        if newDf is None:
            model = None
        else:
            model = myPandasModel(newDf)
        self.slotSwitchTableModel(model)

    def slotSwitchTableModel(self, newModel):
        """
        switch between full .csv model and getMeanDf model
        """
        # print('myTableView.slotSwitchTableModel()')
        self.setModel(newModel)

        if newModel is None:
            return

        colList = newModel._data.columns.values.tolist()
        # print('  todo: slotSwitchTableModel() set all columns to default delegate itemDelegate()')
        for idx, col in enumerate(colList):
            # default delegate is self.tableView.itemDelegate()
            self.setItemDelegateForColumn(idx, self.itemDelegate())

        # install checkboxes in 'incude' column
        if self.doIncludeCheckbox and "include" in colList:
            includeColIndex = colList.index("include")
            print(
                f"_switchTableModel() setting include column {includeColIndex} to myCheckBoxDelegate"
            )
            # self.tableView.setItemDelegateForColumn(includeColIndex, myCheckBoxDelegate(None))
            self.setItemDelegateForColumn(includeColIndex, self.keepCheckBoxDelegate)


# class myMplCanvas(FigureCanvas):
class myMplCanvas(QtWidgets.QFrame):
    """
    hold an fig/plot canvas, in scatter plot we can have 1-4 of these
    """

    signalSelectFromPlot = QtCore.Signal(object)
    signalSelectSquare = QtCore.Signal(object, object)

    def __init__(self, plotNumber=None, parent=None):
        super(myMplCanvas, self).__init__(parent)  # FigureCanvas

        # self.plotType = None

        # self.setFrameWidth(5)
        # self.setStyleSheet("background-color: rgb(0, 255, 0)")
        # self.setContentsMargins(0, 0, 0, 0)
        # self.setContentsMargins(-20, 0, 5, 0)
        self.setFrameShape(QtWidgets.QFrame.Box)
        self.setLineWidth(0)

        pal = self.palette()
        pal.setColor(QtGui.QPalette.WindowText, QtGui.QColor("red"))
        self.setPalette(pal)

        #
        self.plotNumber = plotNumber
        self.stateDict = None
        self.plotDf = None
        self.whatWeArePlotting = None
        # self.mplCursorHover = None

        # needed to show canvas in widget
        self.layout = QtWidgets.QVBoxLayout()  # any will do

        self.fig = Figure(constrained_layout=True)

        self.canvas = FigureCanvas(self.fig)

        self.canvas.axes = self.fig.add_subplot(111)  # was this
        # self.canvas.axes = self.fig.add_axes([0.1, 0.1, 0.9, 0.9]) # [x, y, w, h]
        # gs1 = gridspec.GridSpec(1, 1)
        # gs1.tight_layout(self.fig, rect=[0.5, 0, 1, 1], h_pad=0.5)
        # self.canvas.axes = self.fig.add_subplot(gs1[0])

        self.cid2 = self.canvas.mpl_connect("pick_event", self.on_pick_event)

        self.scatterPlotSelection = None

        self.mplToolbar = NavigationToolbar2QT(
            self.canvas, self.canvas
        )  # params are (canvas, parent)
        self.mplToolbar.hide()  # initially hidden

        self.canvas.setSizePolicy(
            QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
        )
        self.canvas.updateGeometry()

        # fig.subplots_adjust(wspace=0.3, hspace=0.3)
        # self.fig.tight_layout(pad = 3.0) # Padding between the figure edge and the edges of subplots, as a fraction of the font size.
        # self.fig.subplots_adjust(left=0.15,right=0.9,
        #                            bottom=0.1,top=0.9,
        #                            hspace=0.2,wspace=0.2)

        #
        self.layout.addWidget(self.canvas)
        self.setLayout(self.layout)

    def buildUI(self):
        pass

    def mousePressEvent(self, event):
        print("myMplCanvas.mousePressEvent()")
        print(
            "  todo: set all controls to match the plot just clicked on using self.stateDict!!!"
        )
        # print('  stateDict:', self.stateDict)
        self.signalSelectSquare.emit(self.plotNumber, self.stateDict)

    def contextMenuEvent(self, event):
        print("myMplCanvas.contextMenuEvent()")
        contextMenu = QtWidgets.QMenu(self)
        saveAsAction = contextMenu.addAction("Save As...")
        action = contextMenu.exec_(self.mapToGlobal(event.pos()))
        if action == saveAsAction:
            print("todo: save as")
            self.fig.savefig("", dpi=600)

    def on_pick_event(self, event):
        try:
            print("myMplCanvas.on_pick_event() event:", event)
            print("  event.ind:", event.ind)

            if len(event.ind) < 1:
                return
            spikeNumber = event.ind[0]
            print("  selected:", spikeNumber)

            # propagate a signal to parent
            # self.myMainWindow.mySignal('select spike', data=spikeNumber)
            # self.selectSpike(spikeNumber)
        except AttributeError as e:
            pass

    def onPick(self, event):
        """
        when user clicks on a point in the graph

        todo: this makes perfect sense for scatter but maybe not other plots???
        """
        print("=== myMplCanvas.onPick()")  #' event:', type(event), event)
        line = event.artist

        # filter out clicks on 'Annotation' used by mplcursors
        try:
            # when Scatter, line is 'PathCollection', a list of (x,y)
            offsets = line.get_offsets()
        except AttributeError as e:
            return

        ind = event.ind  # ind is a list []
        if len(ind) == 0:
            return
        ind = ind[0]

        # ind is the ith element in (x,y) list of offsets
        # ind 10 (0 based) is index 11 (1 based) in table list
        print(f"  selected from plot ind:{ind}, offsets values are {offsets[ind]}")
        selectDict = self.getAnnotation(ind)

        # to do, just put copy of state dict ???
        selectDict["plotType"] = self.stateDict["plotType"]
        selectDict["dataType"] = self.stateDict["dataType"]
        #
        # emit
        print("  myMplCanvas.signalSelectFromPlot.emit()", selectDict)
        self.signalSelectFromPlot.emit(selectDict)

    def _selectInd(self, ind):
        """
        visually select a point in scatter plot
        """
        print("myMplCanvas._selectInd() ind:", ind)
        if ind > len(self.plotDf) - 1:
            return
        xVal = self.plotDf.at[ind, self.stateDict["xStat"]]
        yVal = self.plotDf.at[ind, self.stateDict["yStat"]]
        if self.scatterPlotSelection is not None:
            print("  scatterPlotSelection x:", xVal, "y:", yVal)
            self.scatterPlotSelection.set_data(xVal, yVal)
        self.fig.canvas.draw()

    def getAnnotation(self, ind):
        if not np.issubdtype(ind, np.integer):
            print("myMplCanvas.getAnnotation() got bad ind:", ind, type(ind))
            return

        xStat = self.stateDict["xStat"]
        yStat = self.stateDict["yStat"]
        groupByColumnName = self.stateDict["groupByColumnName"]

        analysisName = self.plotDf.at[ind, groupByColumnName]
        index = self.plotDf.at[ind, "index"]
        try:
            region = self.plotDf.at[ind, "Region"]  # not all will have this
        except KeyError as e:
            region = "n/a"
        xVal = self.plotDf.at[ind, xStat]
        yVal = self.plotDf.at[ind, yStat]

        returnDict = {
            "ind": ind,
            "index": index,
            "analysisName": analysisName,
            "region": region,
            "xVal": xVal,
            "yVal": yVal,
            #'plotDf': self.plotDf, # potentially very big
        }
        return returnDict

    def slotSelectSquare(self, plotNumber, stateDict):
        # print('myMplCanvas.slotSelectSquare()', plotNumber, 'self.plotNumber:', self.plotNumber)
        if plotNumber == self.plotNumber:
            self.setLineWidth(1)
        else:
            self.setLineWidth(0)

    def slotSelectInd(self, selectDict):
        if self.stateDict["plotType"] == selectDict["plotType"]:
            # only select if same plot type, o/w selections are out of synch
            self._selectInd(selectDict["ind"])

    def slotCancelSelection(self):
        if self.scatterPlotSelection is not None:
            self.scatterPlotSelection.set_data([], [])
        # self.draw()
        # cancel mplCursorHover hover selection
        # if self.mplCursorHover is not None:
        #     selections = self.mplCursorHover.selections
        #     if len(selections) == 1:
        #         self.mplCursorHover.remove_selection(selections[0])

        # never cancel red square, we always wan't one
        # self.slotSelectSquare(None)

        #
        # self.draw() # to update hover
        self.fig.canvas.draw()

    def updateTheme(self):
        """
        redraw using stored self.state
        """
        self.myUpdate(stateDict=None)

    def myUpdateGlobal(self, stateDict):
        """
        update globals but do not plot
        """
        self.canvas.axes.legend().set_visible(stateDict["showLegend"])

        if stateDict["showMplToolbar"]:
            self.mplToolbar.show()
        else:
            self.mplToolbar.hide()

        # if stateDict["doHover"] and self.whatWeArePlotting is not None:
        #     self.mplCursorHover = mplcursors.cursor(self.whatWeArePlotting, hover=True)

        #     @self.mplCursorHover.connect("add")
        #     def _(sel):
        #         # sel.annotation.get_bbox_patch().set(fc="white")
        #         sel.annotation.arrow_patch.set(
        #             arrowstyle="simple", fc="white", alpha=0.5
        #         )
        #         # row in df is from sel.target.index
        #         # print('sel.target.index:', sel.target.index)
        #         ind = sel.target.index
        #         annotationDict = self.getAnnotation(ind)
        #         myText = ""
        #         for k, v in annotationDict.items():
        #             myText += f"{k}: {v}\n"
        #         sel.annotation.set_text(myText)

        # elif not stateDict["doHover"]:
        #     # cancel mplCursorHover hover selection
        #     if self.mplCursorHover is not None:
        #         selections = self.mplCursorHover.selections
        #         if len(selections) == 1:
        #             self.mplCursorHover.remove_selection(selections[0])

        #
        # self.draw() # to update hover
        self.fig.canvas.draw()

    def myUpdate(self, stateDict=None):
        """
        update plot based on control interface
        """

        # store stateDict so we can replot on changing dark theme
        if stateDict is None and self.stateDict is not None:
            # re-use our stateDict
            stateDict = self.stateDict
        else:
            self.stateDict = stateDict.copy()

        dataType = stateDict["dataType"]
        hue = stateDict["hue"]
        groupByColumnName = stateDict["groupByColumnName"]

        plotType = stateDict["plotType"]
        # self.plotType = plotType

        xStatHuman = stateDict["xStatHuman"]
        yStatHuman = stateDict["yStatHuman"]

        xStat = stateDict["xStat"]
        yStat = stateDict["yStat"]

        """
        print('=== myMplCanvas.myUpdate()')
        print('  ', plotType)
        print('  ', 'xStatHuman:', xStatHuman, 'yStatHuman:', yStatHuman)
        print('  ', 'xStat:', xStat, 'yStat:', yStat)
        """

        xIsCategorical = stateDict["xIsCategorical"]
        yIsCategorical = stateDict["yIsCategorical"]

        masterDf = stateDict["masterDf"]
        meanDf = stateDict["meanDf"]

        self.plotDf = meanDf

        self.canvas.axes.clear()

        picker = 5
        if plotType in ["Scatter Plot", "Scatter + Raw + Mean"]:
            # scatter plot user selection
            (self.scatterPlotSelection,) = self.canvas.axes.plot(
                [], [], "oy", markersize=12, fillstyle="none"
            )

            # main scatter
            try:
                self.whatWeArePlotting = sns.scatterplot(
                    x=xStat,
                    y=yStat,
                    hue=hue,
                    data=meanDf,
                    ax=self.canvas.axes,
                    picker=picker,
                    zorder=0,
                )
            except ValueError as e:
                self.fig.canvas.draw()
                print('  EXCEPTION: in myUpdate() "Scatter Plot", exception is:')
                print("  ", e)
                print("  ", "hue:", hue)

            # sem in both x and y, pulling from masterDf
            if dataType == "File Mean" or plotType == "Scatter + Raw + Mean":
                # we need to do this for each hue???
                # if x or y is in categorical (e.g. a string) then do not do this ...
                if xIsCategorical or yIsCategorical:
                    pass
                else:
                    print(
                        "  grabbing mean +- sem for self.groupByColumnName:",
                        groupByColumnName,
                    )
                    color = "k"
                    xd = masterDf.groupby(groupByColumnName).mean()[xStat]
                    xerrd = masterDf.groupby(groupByColumnName).sem()[xStat]
                    yd = masterDf.groupby(groupByColumnName).mean()[yStat]
                    yerrd = masterDf.groupby(groupByColumnName).sem()[yStat]
                    self.canvas.axes.errorbar(
                        xd,
                        yd,
                        xerr=xerrd,
                        yerr=yerrd,
                        fmt="none",
                        capsize=0,
                        zorder=10,
                        color=color,
                        alpha=0.5,
                    )

        elif plotType == "Histogram":
            yStatHuman = "Count"
            doKde = False  # stateDict['doKDE']
            try:
                g = sns.histplot(
                    x=xStat,
                    hue=hue,
                    kde=doKde,
                    data=meanDf,
                    ax=self.canvas.axes,
                    picker=picker,
                )
            except ValueError as e:
                self.fig.canvas.draw()
                print("EXCEPTIONin Histogram:", e)

        elif plotType == "Cumulative Histogram":
            yStatHuman = "Probability"
            try:
                g = sns.histplot(
                    x=xStat,
                    hue=hue,
                    cumulative=True,
                    stat="density",
                    element="step",
                    fill=False,
                    common_norm=False,
                    data=meanDf,
                    ax=self.canvas.axes,
                    picker=picker,
                )
            except ValueError as e:
                self.fig.canvas.draw()
                print("EXCEPTION in Cumulative Histogram:", e)

        elif plotType == "Cumulative Histogram":
            yStatHuman = "Probability"
            try:
                g = sns.histplot(
                    x=xStat,
                    hue=hue,
                    cumulative=True,
                    stat="density",
                    element="step",
                    fill=False,
                    common_norm=False,
                    data=meanDf,
                    ax=self.canvas.axes,
                    picker=picker,
                )
            except ValueError as e:
                self.fig.canvas.draw()
                print("EXCEPTION in Cumulative Histogram:", e)

        elif plotType == "Violin Plot":
            if not xIsCategorical:
                warningStr = "Violin plot requires a categorical x statistic"
            else:
                g = sns.violinplot(
                    x=xStat, y=yStat, hue=hue, data=meanDf, ax=self.canvas.axes
                )

        elif plotType == "Box Plot":
            if not xIsCategorical:
                warningStr = "Box plot requires a categorical x statistic"
            else:
                g = sns.boxplot(
                    x=xStat, y=yStat, hue=hue, data=meanDf, ax=self.canvas.axes
                )

        elif plotType == "Raw + Mean Plot":
            if not xIsCategorical:
                warningStr = "Raw + Mean plot requires a categorical x statistic"
            else:
                try:
                    # does not work here for categorical x
                    # self.scatterPlotSelection, = self.canvas.axes[0].plot([], [], 'oy',
                    #                markersize=12, fillstyle='none')

                    """
                    colorList = [('red'), ('green'), 'b', 'c', 'm', 'y']
                    hueList = meanDf[hue].unique()
                    palette = {}
                    for idx, hue in enumerate(hueList):
                        palette[hue] = colorList[idx]
                    print(palette)
                    """

                    palette = sns.color_palette("Paired")
                    # palette = ['r', 'g', 'b']

                    # stripplot
                    # g = sns.swarmplot(x=xStat, y=yStat,
                    g = sns.stripplot(
                        x=xStat,
                        y=yStat,
                        hue=hue,
                        palette=palette,
                        data=meanDf,
                        ax=self.canvas.axes,
                        # color = color,
                        dodge=True,
                        alpha=0.6,
                        picker=picker,
                        zorder=1,
                    )

                    # g.legend().remove()
                    self.canvas.axes.legend().remove()

                    handles, labels = self.canvas.axes.get_legend_handles_labels()
                    l = self.canvas.axes.legend(
                        handles[0:2],
                        labels[0:2],
                        bbox_to_anchor=(1.05, 1),
                        loc=2,
                        borderaxespad=0.0,
                    )

                    """
                    if self.darkTheme:
                        color = 'w'
                    else:
                        color = 'k'
                    color = [color] * len(hueList)
                    print('color:', color)
                    """

                    self.whatWeArePlotting = sns.pointplot(
                        x=xStat,
                        y=yStat,
                        hue=hue,
                        # palette=palette,
                        data=meanDf,
                        estimator=np.nanmean,
                        ci=68,
                        capsize=0.1,
                        ax=self.canvas.axes,
                        color="r",
                        # legend='full',
                        zorder=10,
                    )

                except ValueError as e:
                    print('EXCEPTION in "Raw + Mean Plot":', e)
                    traceback.print_exc()

        elif plotType == "Regression Plot":
            # regplot does not have hue
            if xIsCategorical or yIsCategorical:
                warningStr = "Regression plot requires continuous x and y statistics"
            else:
                # todo: loop and make a regplot
                # for each unique() name in
                # hue (like Region, Sex, Condition)
                hueList = masterDf[hue].unique()
                for oneHue in hueList:
                    if oneHue == "None":
                        continue
                    tmpDf = meanDf[meanDf[hue] == oneHue]
                    # print('regplot oneHue:', oneHue, 'len(tmpDf)', len(tmpDf))
                    sns.regplot(x=xStat, y=yStat, data=tmpDf, ax=self.canvas.axes)
        else:
            print("  did not understand plot type:", plotType)

        #
        # update
        self.canvas.axes.figure.canvas.mpl_connect("pick_event", self.onPick)

        # self.mplCursorHover = None
        # if stateDict["doHover"] and self.whatWeArePlotting is not None:
        #     self.mplCursorHover = mplcursors.cursor(self.whatWeArePlotting, hover=True)

        #     @self.mplCursorHover.connect("add")
        #     def _(sel):
        #         # sel.annotation.get_bbox_patch().set(fc="white")
        #         sel.annotation.arrow_patch.set(
        #             arrowstyle="simple", fc="white", alpha=0.5
        #         )
        #         # row in df is from sel.target.index
        #         # print('sel.target.index:', sel.target.index)
        #         ind = sel.target.index
        #         annotationDict = self.getAnnotation(ind)
        #         myText = ""
        #         for k, v in annotationDict.items():
        #             myText += f"{k}: {v}\n"
        #         sel.annotation.set_text(myText)

        #
        # self.mySetStatusBar(warningStr)

        self.canvas.axes.spines["right"].set_visible(False)
        self.canvas.axes.spines["top"].set_visible(False)

        if not stateDict["showLegend"]:
            self.canvas.axes.legend().remove()

        # print('myUpdate() self.plotSize:', self.plotSize)
        self.canvas.axes.set_xlabel(xStatHuman)
        self.canvas.axes.set_ylabel(yStatHuman)
        """
        if self.plotSize == 'paper':
            fontsize = 10
            self.canvas.axes[0].set_xlabel(xStatHuman, fontsize=fontsize)
            self.canvas.axes[0].set_ylabel(yStatHuman, fontsize=fontsize)
        else:
            self.canvas.axes[0].set_xlabel(xStatHuman)
            self.canvas.axes[0].set_ylabel(yStatHuman)
        """

        # subplots_adjust
        # self.fig.canvas.draw_idle()
        self.fig.canvas.draw()


class bScatterPlotMainWindow(QtWidgets.QMainWindow):
    # send_fig = QtCore.pyqtSignal(str)
    signalStateChange = QtCore.Signal(object)
    signalSelectFromPlot = QtCore.Signal(object)
    signalCancelSelection = QtCore.Signal()
    signalMeanModelChange = QtCore.Signal(object)
    signal_xModelChange = QtCore.Signal(object)
    signal_yModelChange = QtCore.Signal(object)

    def __init__(
        self,
        path,
        categoricalList,
        hueTypes,
        analysisName,
        sortOrder=None,
        statListDict=None,
        interfaceDefaults=None,
        masterDf=None,
        parent=None,
    ):
        """
        path: full path to .csv file generated with reanalyze
        categoricalList: specify columns that are categorical
            would just like to use 'if column is string' but sometimes number like 1/2/3 need to be categorical
        hueTypes:
        analysisName: column used for group by
        sortOrder:
        statListDict: dict where keys are human readable stat names
                    that map onto 'yStat' to specify column in csv
        interfaceDefaults: specify key/value in dict to set state of interface popups/etc
        masterDf: if not none then use it (rather than loading csv path)
                    used by main sanpy interface
        parent: not used, parent sanpy app

        todo: make pure text columns categorical
        todo: remove analysisName and add as 'groupby' popup from categorical columns
            depending on 'analysisName' popup, group differently in getMeanDf
        """
        super(bScatterPlotMainWindow, self).__init__(parent)

        # self.keepCheckBoxDelegate = myCheckBoxDelegate(None)

        if interfaceDefaults is None:
            interfaceDefaults = myInterfaceDefaults
        self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+w"), self)
        self.shortcut.activated.connect(self.myCloseAction)

        self.statusBar = QtWidgets.QStatusBar()
        self.setStatusBar(self.statusBar)

        # self.mplCursorHover = None

        self.buildMenus()

        # assigns self.masterDf
        # if masterDf is not None will use masterDf rather than loading path
        self.loadPath(path, masterDf=masterDf, categoricalList=categoricalList)

        # statListDict is a dict with key=humanstat name and yStat=column name in csv
        # self.statListDict = sanpy.bAnalysisUtil.getStatList()
        # 20210305 done in loadPath()
        if statListDict is not None:
            self.statListDict = statListDict
            # append all categorical
            if categoricalList is not None:
                for categorical in categoricalList:
                    self.statListDict[categorical] = {"yStat": categorical}
        """
        if statListDict is None:
            # build statListDict from columns of csv path
            self.statListDict = {}
            for colStr in self.masterDfColumns:
                self.statListDict[colStr] = {'yStat': colStr}
        else:
            self.statListDict = statListDict
        """

        # categoricalList = ['Condition', 'Sex', 'Region', 'File Number', 'File Name']
        # 20210305 done in load path
        """
        for categorical in categoricalList:
            self.statListDict[categorical] = {'yStat': categorical}
        # this was originally in self.load() ???
        self.masterCatColumns = categoricalList
        """

        """
        self.stateDict = {}
        self.stateDict['groupByColumnName'] = analysisName
        self.stateDict['sortOrder'] = sortOrder
        self.stateDict['hue'] = hue
        self.stateDict['darkTheme'] = True
        self.stateDict['doKDE'] = False
        self.stateDict['doHover'] = False
        self.stateDict['plotSize'] = 'paper'
        self.stateDict['showLegend'] = True
        self.stateDict['plotType'] = 'Scatter Plot'
        self.stateDict['dataType'] = 'File Mean'
        """

        # unique identifyer to group by
        # for sanpy this is 'analysisName', for bImPy this is xxx
        self.groupByColumnName = analysisName
        self.sortOrder = sortOrder

        # 20210112 moved up
        # self.loadPath(path)

        self.whatWeArePlotting = None  # return from sns scatter plot (all plots)
        self.scatterPlotSelection = None
        self.xDf = None  #
        self.yDf = None  #
        self.plotDF = None  # df we are plotting (can be same as mean yDf)
        # use this to get row on self.onPick
        self.plotStatx = None
        self.plotStaty = None

        # self.main_widget = QtWidgets.QWidget(self)

        # this is causing tons of problems/crashes
        if interfaceDefaults["Hue"] is not None:
            self.hue = interfaceDefaults["Hue"]
        else:
            self.hue = "None"  # self.hueTypes[0]

        self.darkTheme = False
        self.doKDE = False  # fits on histogram plots
        self.showMplToolbar = False  # fits on histogram plots

        # self.doHover = False

        self.plotSizeList = ["paper", "talk", "poster"]
        self.plotSize = "paper"
        self.plotLayoutList = ["1x", "1x2", "2x1", "2x2"]
        self.plotLayoutType = "2x2"
        self.updatePlot = None  # which plot to update 1,2,3,4

        self.showLegend = False
        self.plotType = "Scatter Plot"

        self.dataTypes = ["All Spikes", "File Mean"]
        self.dataType = "File Mean"  # (Raw, File Mean)

        self.buildUI(interfaceDefaults=interfaceDefaults)

        # bar = self.menuBar()
        # file = bar.addMenu("Load")

        self.show()

        # self.updatePlotSize() # calls update2()
        self.update2()

    def buildUI(self, interfaceDefaults):
        # if self.darkTheme:
        #     plt.style.use('dark_background')
        if self.darkTheme:
            plt.style.use("dark_background")
        else:
            plt.rcParams.update(plt.rcParamsDefault)

        # HBox for control and plot grid
        self.hBoxLayout = QtWidgets.QHBoxLayout(self)

        # this is confusing, beacaue we are a QMainWindow
        # we need to create a central widget, set its layout
        # and then set the central widget of self (QMainWindow)
        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(self.hBoxLayout)
        self.setCentralWidget(centralWidget)

        # switch to 2x2
        """
        # allow 1-4 plots
        tmpNumRow = 1
        tmpNumCol = 1
        numPlot = tmpNumRow * tmpNumCol

        self.fig = Figure()
        self.fig.tight_layout()
        # don't use subplot, each plot will be its own widget using myMplCanvas
        self.axes = [self.fig.add_subplot(tmpNumRow, tmpNumCol, i+1) for i in range(numPlot)]
        self.canvas = FigureCanvas(self.fig) # what is added to pyqt interface
        self.cid = self.canvas.mpl_connect('pick_event', self.on_pick_event)
        # maybe use
        #self.toolbar = NavigationToolbar(self.canvas, self)
        self.mplToolbar = NavigationToolbar2QT(self.canvas, self.canvas) # params are (canvas, parent)

        self.canvas.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                                  QtWidgets.QSizePolicy.Expanding)
        self.canvas.updateGeometry()
        """

        # to hold popups
        self.layout = QtWidgets.QGridLayout()
        self.hBoxLayout.addLayout(self.layout)

        def _defaultDropdownIdx(keyList, name):
            # if name not in keys, return 0
            theRet = 0
            try:
                theRet = keyList.index(name)
            except ValueError as e:
                print(
                    f'WARNING: _defaultDropdownIdx() did not find "{name}"" in keyList: {keyList}'
                )
                theRet = 0
            return theRet

        keys = list(self.statListDict.keys())
        # keys += self.hueTypes
        self.xDropdown = QtWidgets.QComboBox()
        self.xDropdown.addItems(keys)
        if interfaceDefaults["X Statistic"] is not None:
            defaultIdx = _defaultDropdownIdx(keys, interfaceDefaults["X Statistic"])
        else:
            defaultIdx = 0
        self.xDropdown.setCurrentIndex(defaultIdx)

        self.yDropdown = QtWidgets.QComboBox()
        self.yDropdown.addItems(keys)
        if interfaceDefaults["Y Statistic"] is not None:
            defaultIdx = _defaultDropdownIdx(keys, interfaceDefaults["Y Statistic"])
        else:
            defaultIdx = 0
        self.yDropdown.setCurrentIndex(defaultIdx)

        #
        # hue, to control colors in plot
        hueList = ["None"] + self.hueTypes  # prepend 'None'
        self.hueDropdown = QtWidgets.QComboBox()
        self.hueDropdown.addItems(hueList)
        if interfaceDefaults["Hue"] is not None:
            defaultIdx = _defaultDropdownIdx(hueList, interfaceDefaults["Hue"])
        else:
            defaultIdx = 0
        self.hueDropdown.setCurrentIndex(defaultIdx)  # 1 because we pre-pended 'None'
        self.hueDropdown.currentIndexChanged.connect(self.updateHue)

        #
        # group by, to control grouping in table
        # todo: get 'None' implemented
        # groupByList = ['None'] + self.hueTypes # prepend 'None'
        groupByList = self.hueTypes
        self.groupByDropdown = QtWidgets.QComboBox()
        self.groupByDropdown.addItems(groupByList)
        if interfaceDefaults["Group By"] is not None:
            defaultIdx = _defaultDropdownIdx(groupByList, interfaceDefaults["Group By"])
        else:
            defaultIdx = 0
        self.groupByDropdown.setCurrentIndex(
            defaultIdx
        )  # 1 because we pre-pended 'None'
        self.groupByDropdown.currentIndexChanged.connect(self.updateGroupBy)

        # color
        # colorTypes = ['Region', 'Sex', 'Condition', 'File Number'] #, 'File Name'] #, 'None']
        # self.color = 'Region'
        """
        self.color = self.colorTypes[0]
        self.colorDropdown = QtWidgets.QComboBox()
        self.colorDropdown.addItems(self.colorTypes)
        self.colorDropdown.setCurrentIndex(0)
        self.colorDropdown.currentIndexChanged.connect(self.updateColor)
        """

        self.typeDropdown = QtWidgets.QComboBox()
        self.typeDropdown.addItems(
            [
                "Scatter Plot",
                "Scatter + Raw + Mean",
                "Regression Plot",
                "Violin Plot",
                "Box Plot",
                "Raw + Mean Plot",
                "Histogram",
                "Cumulative Histogram",
            ]
        )
        self.typeDropdown.setCurrentIndex(0)

        self.dataTypeDropdown = QtWidgets.QComboBox()
        self.dataTypeDropdown.setToolTip(
            "All Spikes is all spikes \n File Mean is the mean within each analysis file"
        )
        self.dataTypeDropdown.addItems(self.dataTypes)
        self.dataTypeDropdown.setCurrentIndex(1)

        self.xDropdown.currentIndexChanged.connect(self.update2)
        self.yDropdown.currentIndexChanged.connect(self.update2)
        self.typeDropdown.currentIndexChanged.connect(self.updatePlotType)
        self.dataTypeDropdown.currentIndexChanged.connect(self.updateDataType)

        showLegendCheckBox = QtWidgets.QCheckBox("Legend")
        showLegendCheckBox.setChecked(self.showLegend)
        showLegendCheckBox.stateChanged.connect(self.setShowLegend)

        # swap the sort order
        swapSortButton = QtWidgets.QPushButton("Swap Sort Order")
        swapSortButton.clicked.connect(self.setSwapSort)
        # works fine
        """
        darkThemeCheckBox = QtWidgets.QCheckBox('Dark Theme')
        darkThemeCheckBox.setChecked(self.darkTheme)
        darkThemeCheckBox.stateChanged.connect(self.setTheme)
        darkThemeCheckBox.setDisabled(True)
        darkThemeCheckBox.setEnabled(False)
        """

        """
        kdeCheckBox = QtWidgets.QCheckBox('kde (hist)')
        kdeCheckBox.setChecked(self.doKDE)
        kdeCheckBox.stateChanged.connect(self.setKDE)
        """
        mplToolbar = QtWidgets.QCheckBox("Toolbar")
        mplToolbar.setChecked(self.showMplToolbar)
        mplToolbar.stateChanged.connect(self.setMplToolbar)

        # hoverCheckbox = QtWidgets.QCheckBox("Hover Info")
        # hoverCheckbox.setChecked(self.doHover)
        # hoverCheckbox.stateChanged.connect(self.setHover)

        # work fine
        """
        self.plotSizeDropdown = QtWidgets.QComboBox()
        self.plotSizeDropdown.setToolTip('Set size of fonts for paper, talk, or poster')
        self.plotSizeDropdown.addItems(self.plotSizeList)
        self.plotSizeDropdown.setCurrentIndex(0) # paper
        self.plotSizeDropdown.currentIndexChanged.connect(self.updatePlotSize)
        self.plotSizeDropdown.setDisabled(True)
        """

        # works fine
        """
        self.plotLayoutDropdown = QtWidgets.QComboBox()
        self.plotLayoutDropdown.setToolTip('1, 2, or 4 plots')
        self.plotLayoutDropdown.addItems(self.plotLayoutList)
        self.plotLayoutDropdown.setCurrentIndex(3) #
        self.plotLayoutDropdown.currentIndexChanged.connect(self.updatePlotLayout)
        self.plotLayoutDropdown.setDisabled(True)
        """

        # not needed beccause we now clik plot to show red square
        """
        self.plotUpdateList = ['1', '2', '3', '4']
        self.plotUpdateDropdown = QtWidgets.QComboBox()
        self.plotUpdateDropdown.setToolTip('Which plot to update')
        self.plotUpdateDropdown.addItems(self.plotUpdateList)
        self.plotUpdateDropdown.setCurrentIndex(0) # paper
        self.plotUpdateDropdown.currentIndexChanged.connect(self.updatePlotUpdate)
        """

        self.layout.addWidget(QtWidgets.QLabel("X Statistic"), 0, 0)
        self.layout.addWidget(self.xDropdown, 0, 1)
        self.layout.addWidget(QtWidgets.QLabel("Y Statistic"), 1, 0)
        self.layout.addWidget(self.yDropdown, 1, 1)
        self.layout.addWidget(QtWidgets.QLabel("Hue"), 2, 0)
        self.layout.addWidget(self.hueDropdown, 2, 1)
        self.layout.addWidget(QtWidgets.QLabel("Group By"), 3, 0)
        self.layout.addWidget(self.groupByDropdown, 3, 1)
        # self.layout.addWidget(QtWidgets.QLabel("Color"), 3, 0)
        # self.layout.addWidget(self.colorDropdown, 3, 1)
        #
        self.layout.addWidget(QtWidgets.QLabel("Plot Type"), 0, 2)
        self.layout.addWidget(self.typeDropdown, 0, 3)
        self.layout.addWidget(QtWidgets.QLabel("Data Type"), 1, 2)
        self.layout.addWidget(self.dataTypeDropdown, 1, 3)
        self.layout.addWidget(showLegendCheckBox, 2, 2)
        # works fine
        # self.layout.addWidget(darkThemeCheckBox, 2, 3)
        self.layout.addWidget(swapSortButton, 2, 3)
        self.layout.addWidget(mplToolbar, 3, 2)
        # self.layout.addWidget(hoverCheckbox, 3, 3)
        # works fine
        # self.layout.addWidget(QtWidgets.QLabel("Plot Size"), 4, 2)
        # self.layout.addWidget(self.plotSizeDropdown, 4, 3)
        # works fine
        # self.layout.addWidget(QtWidgets.QLabel("Plot Layout"), 4, 0) # out of order
        # self.layout.addWidget(self.plotLayoutDropdown, 4, 1)

        # self.layout.addWidget(QtWidgets.QLabel("Update Plot"), 5, 0) # out of order
        # self.layout.addWidget(self.plotUpdateDropdown, 5, 1)

        nextRow = 5  # for text table
        rowSpan = 1
        colSpan = 4
        # self.layout.addWidget(self.canvas, 3, 0, rowSpan, colSpan)

        #
        # grid of plots (myMplCanvas)
        self.plotLayout = QtWidgets.QGridLayout()
        self.plotLayout.setContentsMargins(0, 0, 0, 0)
        self.plotLayout.setHorizontalSpacing(0)
        self.plotLayout.setVerticalSpacing(0)

        self.myPlotCanvasList = [None] * 4

        self.updatePlotLayoutGrid()

        # append
        self.hBoxLayout.addLayout(self.plotLayout)

        """
        self.myToolbar = QtWidgets.QToolBar()
        self.myToolbar.setFloatable(True)
        self.myToolbar.setMovable(True)
        self.tmpToolbarAction = self.myToolbar.addWidget(self.canvas)
        self.addToolBar(QtCore.Qt.RightToolBarArea, self.myToolbar)
        """

        tabwidget = QtWidgets.QTabWidget()

        # hold main df database (imutable except for 'include'
        self.rawTableView = myTableView("All Spikes")
        self.rawTableView.clicked.connect(self.slotTableViewClicked)
        self.signalCancelSelection.connect(self.rawTableView.clearSelection)
        self.signalSelectFromPlot.connect(self.rawTableView.slotSelectRow)
        # never switch rawTableView model, dust update column 'include'
        # self.signalMeanModelChange.connect(self.rawTableView.slotSwitchTableModel)
        self.rawTableView.slotSwitchTableDf(self.masterDf)

        # self.xDf
        self.xTableView = myTableView("File Mean")
        self.xTableView.clicked.connect(self.slotTableViewClicked)
        self.signalCancelSelection.connect(self.xTableView.clearSelection)
        self.signalSelectFromPlot.connect(self.xTableView.slotSelectRow)
        # never switch rawTableView model, dust update column 'include'
        # self.signalMeanModelChange.connect(self.rawTableView.slotSwitchTableModel)
        self.xTableView.slotSwitchTableDf(self.xDf)

        # self.yDf
        self.yTableView = myTableView("File Mean")
        self.yTableView.clicked.connect(self.slotTableViewClicked)
        self.signalCancelSelection.connect(self.yTableView.clearSelection)
        self.signalSelectFromPlot.connect(self.yTableView.slotSelectRow)
        # never switch rawTableView model, dust update column 'include'
        # self.signalMeanModelChange.connect(self.rawTableView.slotSwitchTableModel)
        self.yTableView.slotSwitchTableDf(self.yDf)

        # todo: make this mean across yDf with stat test
        # mean table with pandas dataframe
        """
        self.tableView = myTableView('Not Sure')
        self.tableView.clicked.connect(self.slotTableViewClicked)
        self.signalCancelSelection.connect(self.tableView.clearSelection)
        self.signalSelectFromPlot.connect(self.tableView.slotSelectRow)
        self.signalMeanModelChange.connect(self.tableView.slotSwitchTableModel)
        """

        #
        # self.layout.addWidget(self.tableView, nextRow, 0, rowSpan, colSpan)
        tabwidget.addTab(self.rawTableView, "Raw")
        # tabwidget.addTab(self.tableView, "Mean")
        tabwidget.addTab(self.xTableView, "X-Stats")
        tabwidget.addTab(self.yTableView, "Y-Stats")
        self.layout.addWidget(tabwidget, nextRow, 0, rowSpan, colSpan)

    def updatePlotLayoutGrid(self):
        """
        use this to switch between (1x, 1x2, 2x1, 2x2)
        """

        print("updatePlotLayoutGrid()")
        plotLayoutType = self.plotLayoutType  # 1x, 1x2, 2x1, 2x2
        if plotLayoutType == "1x":
            numPlots = 1
        elif plotLayoutType == "1x2":
            numPlots = 2
        elif plotLayoutType == "2x1":
            numPlots = 2
        elif plotLayoutType == "2x2":
            numPlots = 4

        # remove all widgets from self.plotLayout
        n = self.plotLayout.count()
        for i in range(n):
            item = self.plotLayout.itemAt(i)
            if item is None:
                print("  warning: updatePlotLayoutGrid() got None item at step", i)
                continue
            widget = item.widget()
            print("  updatePlotLayoutGrid() removing i:", i, "item:", type(item))
            self.plotLayout.removeWidget(widget)
            # self.plotLayout.removeItem(item)

        state = self.getState()
        for i in range(numPlots):
            if i == 0:
                row = 0
                col = 0
            elif i == 1:
                if plotLayoutType == "1x2":
                    row = 0
                    col = 1
                elif plotLayoutType == "2x1":
                    row = 1
                    col = 0
                elif plotLayoutType == "2x2":
                    row = 0
                    col = 1
            elif i == 2:
                row = 1
                col = 0
            elif i == 3:
                row = 1
                col = 1
            #
            oneCanvas = myMplCanvas(plotNumber=i)
            oneCanvas.myUpdate(state)  # initial plot
            self.signalCancelSelection.connect(oneCanvas.slotCancelSelection)
            self.myPlotCanvasList[i] = oneCanvas

            #
            self.plotLayout.addWidget(oneCanvas, row, col)

        # connect each canvas to all other canvas
        for i in range(numPlots):
            iCanvas = self.myPlotCanvasList[i]
            iCanvas.signalSelectSquare.connect(self.slotSelectSquare)
            iCanvas.signalSelectFromPlot.connect(self.slotSelectFromPlot)
            for j in range(numPlots):
                # if i==j:
                #    continue
                jCanvas = self.myPlotCanvasList[j]
                iCanvas.signalSelectFromPlot.connect(jCanvas.slotSelectInd)
                iCanvas.signalSelectSquare.connect(jCanvas.slotSelectSquare)

        #
        # select the firsr plot
        self.myPlotCanvasList[0].signalSelectSquare.emit(0, None)  # slotSelectSquare(0)

    def keyPressEvent(self, event):
        print("keyPressEvent()")
        if event.key() == QtCore.Qt.Key_Escape:
            self.cancelSelection()
        elif event.type() == QtCore.QEvent.KeyPress and event.matches(
            QtGui.QKeySequence.Copy
        ):
            self.copySelection2()
        #
        event.accept()

    def copySelection2(self):
        dfCopy = None
        if self.xDf is not None:
            dfCopy = self.xDf.copy()
            # self.xDf.to_clipboard(sep='\t', index=False)
            # print('Copied to clipboard')
            # print(self.xDf)
        if self.yDf is not None:
            if dfCopy is None:
                dfCopy = self.yDf.copy()
            else:
                # append row then yDf
                dfCopy.append(pd.Series(), ignore_index=True)
                dfCopy.append(self.yDf, ignore_index=True)
            # self.yDf.to_clipboard(sep='\t', index=False)
            # print('Copied to clipboard')
            # print(self.yDf)
        #
        if dfCopy is not None:
            dfCopy.to_clipboard(sep="\t", index=False)
            print(dfCopy)

    """
    def eventFilter(self, source, event):
        if (event.type() == QtCore.QEvent.KeyPress and
                        event.matches(QtGui.QKeySequence.Copy)):
            self.copySelection2()
            return True
        return super(bScatterPlotMainWindow, self).eventFilter(source, event)
    """

    def cancelSelection(self):
        self.signalCancelSelection.emit()

    def _switchTableModel(self, newModel):
        """
        switch model for mean table (Self.xxx)
        """
        # print('bScatterPlotMainWindow._switchTableModel()')
        self.signalMeanModelChange.emit(newModel)

    def getState(self):
        """
        query all controls and create dict with state

        used by myMplCanvas.update()
        """
        stateDict = {}

        stateDict["dataType"] = self.dataType  # ['All spikes', 'File Mean']
        stateDict["groupByColumnName"] = self.groupByColumnName

        plotType = self.plotType
        stateDict["plotType"] = plotType

        xStatHuman = self.xDropdown.currentText()
        yStatHuman = self.yDropdown.currentText()
        stateDict["xStatHuman"] = xStatHuman
        stateDict["yStatHuman"] = yStatHuman
        xStat = self.statListDict[xStatHuman]["yStat"]  # statListDict always used yStat
        yStat = self.statListDict[yStatHuman]["yStat"]
        stateDict["xStat"] = xStat
        stateDict["yStat"] = yStat
        stateDict["xIsCategorical"] = pd.api.types.is_string_dtype(
            self.masterDf[xStat].dtype
        )
        stateDict["yIsCategorical"] = pd.api.types.is_string_dtype(
            self.masterDf[yStat].dtype
        )

        if self.hue == "None":
            # special case, we do not want None in self.statNameMap
            hue = None
        else:
            hue = self.hue
        stateDict["hue"] = hue

        stateDict["darkTheme"] = self.darkTheme
        stateDict["showLegend"] = self.showLegend
        # stateDict["doHover"] = self.doHover
        stateDict["showMplToolbar"] = self.showMplToolbar

        if self.dataType == "All Spikes":
            meanDf = self.masterDf
            if self.sortOrder is not None:
                # need to sort so onPick works
                print("  ", "(1) sorting by self.sortOrder:", self.sortOrder)
                meanDf = meanDf.sort_values(self.sortOrder)

            # remove rows that have nan in our x or y stat
            meanDf = meanDf[~meanDf[xStat].isnull()]
            meanDf = meanDf[~meanDf[yStat].isnull()]
            meanDf = meanDf.reset_index()

        elif self.dataType == "File Mean":
            meanDf = self.getMeanDf(xStat, yStat)
        else:
            print("error in self.dataType:", self.dataType)

        stateDict["masterDf"] = self.masterDf
        stateDict["meanDf"] = meanDf
        return stateDict

    def myCloseAction(self):
        print("myCloseAction()")

    def mySetStatusBar(self, text):
        self.statusBar.showMessage(text)  # ,2000)

    """
    def copyTable(self):
        headerList = []
        for i in self.tableView.model().columnCount():
            headers.append(self.tableView.model().headerData(i, QtCore.Qt.Horizontal).toString()
        print('copyTable()')
        print('  headers:', headers)
        m = self.tableView.rowCount()
        n = self.tableView.columnCount()
        table = [[''] * n for x in range(m+1)]
        #for i in m:
    """

    """
    def copySelection(self):
        #self.copyTable()

        selection = self.tableView.selectedIndexes()
        if selection:
            rows = sorted(index.row() for index in selection)
            columns = sorted(index.column() for index in selection)
            rowcount = rows[-1] - rows[0] + 1
            colcount = columns[-1] - columns[0] + 1
            table = [[''] * colcount for _ in range(rowcount)]
            for index in selection:
                row = index.row() - rows[0]
                column = index.column() - columns[0]
                table[row][column] = index.data()
            stream = io.StringIO()
            csv.writer(stream).writerows(table)
            QtWidgets.QApplication.clipboard().setText(stream.getvalue())
    """

    def buildMenus(self):
        loadAction = QtWidgets.QAction("Load database.xlsx", self)
        loadAction.triggered.connect(self.loadPathMenuAction)

        menubar = self.menuBar()
        fileMenu = menubar.addMenu("&File")
        fileMenu.addAction(loadAction)

    def loadPathMenuAction(self):
        """
        prompt user for database.xlsx
        run reanalyze.py on that xlsx database
        load resultant _master.csv
        """
        print("loadPathMenuAction")

    def loadPath(self, path, masterDf=None, categoricalList=None):
        """
        path: full path to .csv file generated with reanalyze.py
        """
        # path = '/Users/cudmore/data/laura-ephys/Superior vs Inferior database_master.csv'
        if masterDf is not None:
            self.masterDf = masterDf
        else:
            if path.endswith(".csv"):
                self.masterDf = pd.read_csv(
                    path, header=0
                )  # , dtype={'ABF File': str})
            elif path.endswith(".xls"):
                self.masterDf = pd.read_excel(
                    path, header=0
                )  # , dtype={'ABF File': str})
            elif path.endswith(".xlsx"):
                self.masterDf = pd.read_excel(
                    path, header=0, engine="openpyxl"
                )  # , dtype={'ABF File': str})
            else:
                print(
                    "error: file type not supported. Expecting csv/xls/xlsx. Path:",
                    path,
                )

        # 20210426
        # combine some columns into new encoding
        # like, 'male+superior' as 'ms'
        # moved to reanalyze
        # want to move to reanalyze
        # but my base analysis does not have region/sex/condition etc
        tmpNewCol = "RegSex"
        self.masterDf[tmpNewCol] = ""
        for tmpRegion in ["Superior", "Inferior"]:
            for tmpSex in ["Male", "Female"]:
                newEncoding = tmpRegion[0] + tmpSex[0]
                regSex = self.masterDf[
                    (self.masterDf["Region"] == tmpRegion)
                    & (self.masterDf["Sex"] == tmpSex)
                ]
                regSex = (self.masterDf["Region"] == tmpRegion) & (
                    self.masterDf["Sex"] == tmpSex
                )
                print("newEncoding:", newEncoding, "regSex:", regSex.shape)
                self.masterDf.loc[regSex, tmpNewCol] = newEncoding

        # sys.exit()

        self.masterDfColumns = self.masterDf.columns.to_list()

        #
        # try and guess categorical columns
        # if int64 and unique()<20 then assume it is category (works for date yyyymmdd)
        # print('loadPath() dtypes:')
        # print(self.masterDf.dtypes)
        if categoricalList is None:
            categoricalList = []
            for colStr, dtype in self.masterDf.dtypes.items():
                if dtype == object:
                    print("colStr:", colStr, "is", dtype)
                    categoricalList.append(colStr)
                elif dtype == np.int64:
                    unique = self.masterDf[colStr].unique()
                    numUnique = len(unique)
                    print("colStr:", colStr, "is", dtype, "numUnique:", numUnique)
                    categoricalList.append(colStr)

        self.masterCatColumns = categoricalList
        self.hueTypes = categoricalList

        self.statListDict = {}
        for colStr in self.masterDfColumns:
            self.statListDict[colStr] = {"yStat": colStr}

        # not sure what this was for ???
        # 20210112, put back in if necc
        # self.masterCatColumns = ['Condition', 'File Number', 'Sex', 'Region', 'filename', 'analysisname']
        # self.masterCatColumns = self.categoricalList

        # todo: put this somewhere better
        self.setWindowTitle(path)

    def setSwapSort(self):
        """
        rotate the sort key to be able to sort by different keys, like 'sex' versus 'region'
        """
        # print('=== setSwapSort()')
        # print('  sortOrder was:', self.sortOrder)
        self.sortOrder = self.sortOrder[1:] + self.sortOrder[:1]
        # print('  sortOrder now:', self.sortOrder)
        self.mySetStatusBar(f"Sort order is now: {self.sortOrder}")

    def setShowLegend(self, state):
        print("setShowLegend() state:", state)
        self.showLegend = state
        self.updateGlobal()

    """
    def setKDE(self, state):
        # only used in histograms
        self.doKDE = state
        self.updateGlobal()
    """

    def setMplToolbar(self, state):
        # only used in histograms
        self.showMplToolbar = state
        self.updateGlobal()

    # def setHover(self, state):
    #     # used in scatterplots and point plots
    #     self.doHover = state
    #     # self.signalStateChange.emit(self.getState())
    #     self.updateGlobal()

    def setTheme(self, state):
        print("setTheme() state:", state)

        self.darkTheme = state
        if self.darkTheme:
            plt.style.use("dark_background")
            # sns.set_context('talk')

        else:
            # print(plt.style.available)
            plt.rcParams.update(plt.rcParamsDefault)
            # sns.set_context('paper')

        # self.updateGlobal()
        n = len(self.myPlotCanvasList)
        for i in range(n):
            if self.myPlotCanvasList[i] is not None:
                self.myPlotCanvasList[i].updateTheme()

        ###
        return
        ###

        ###
        ###
        # remove
        # self.plotVBoxLayout.removeWidget(self.toolbar)
        # need this !!! removeWidget does not work
        # self.myToolbar.removeAction(self.tmpToolbarAction)
        self.hBoxLayout.removeWidget(self.canvas)
        self.canvas.setParent(None)

        self.fig = None  # ???
        self.mplToolbar = None
        self.canvas = None

        self.fig = Figure()
        self.canvas = FigureCanvas(self.fig)
        tmpAx = self.fig.add_subplot(111)  # self.ax1 not used
        self.axes = [tmpAx]
        self.cid = self.canvas.mpl_connect("pick_event", self.on_pick_event)
        # matplotlib navigation toolbar
        self.mplToolbar = NavigationToolbar2QT(
            self.canvas, self.canvas
        )  # params are (canvas, parent)
        self.hBoxLayout.addWidget(self.canvas)

        #
        # add a second plot
        if self.canvas2 is not None:
            self.hBoxLayout.removeWidget(self.canvas2)
            self.canvas2.setParent(None)
        self.canvas2 = myMplCanvas()
        self.signalStateChange.connect(self.canvas2.myUpdate)
        tmpState = self.getState()
        self.canvas2.myUpdate(tmpState)
        self.hBoxLayout.addWidget(self.canvas2)

        # original plot
        self.update2()

    def updatePlotLayout(self):
        """
        set number of plots [1x, 1x2, 2x1, 2x2]
        """
        self.plotLayoutType = (
            self.plotLayoutDropdown.currentText()
        )  # ['paper', 'poster', 'talk']

        self.updatePlotLayoutGrid()

        self.update2()

    def updatePlotSize(self):
        self.plotSize = (
            self.plotSizeDropdown.currentText()
        )  # ['paper', 'poster', 'talk']
        sns.set_context(self.plotSize)  # , font_scale=1.4)
        self.updateGlobal()

    def updateHue(self):
        hue = self.hueDropdown.currentText()
        self.hue = hue
        # self.signalStateChange.emit(self.getState())
        self.update2()

    def updateGroupBy(self):
        self.groupByColumnName = self.groupByDropdown.currentText()
        self.update2()  # todo: don't update plot, update table

    """
    def updateColor(self):
        color = self.colorDropdown.currentText()
        self.color = color
        self.update2()
    """

    def updatePlotType(self):
        plotType = self.typeDropdown.currentText()
        self.plotType = plotType
        self.update2()

    def updateDataType(self):
        dataType = self.dataTypeDropdown.currentText()
        self.dataType = dataType
        self.update2()

    """
    def old_on_pick_event(self, event):
        try:
            print('on_pick_event() event:', event)
            print('event.ind:', event.ind)

            if len(event.ind) < 1:
                return
            spikeNumber = event.ind[0]
            print('  selected:', spikeNumber)

            # propagate a signal to parent
            #self.myMainWindow.mySignal('select spike', data=spikeNumber)
            #self.selectSpike(spikeNumber)
        except (AttributeError) as e:
            pass
    """

    def getMeanDf(self, xStat, yStat, verbose=False):
        # need to get all categorical columns from orig df
        # these do not change per file (sex, condition, region)
        print("=== getMeanDf() xStat:", xStat, "yStat:", yStat)
        print("  self.groupByColumnName:", self.groupByColumnName)

        xIsCategorical = pd.api.types.is_string_dtype(self.masterDf[xStat].dtype)
        yIsCategorical = pd.api.types.is_string_dtype(self.masterDf[yStat].dtype)

        groupByNone = self.groupByColumnName == "None"

        aggList = ["count", "mean", "std", "sem", "median"]
        if xIsCategorical or groupByNone:
            self.xDf = None
        else:
            self.xDf = self.masterDf.groupby(self.groupByColumnName, as_index=False)[
                xStat
            ].agg(aggList)
            self.xDf = self.xDf.reset_index()
            self.xDf.insert(1, "stat", xStat)  # column 1, in place

        if yIsCategorical or groupByNone:
            self.yDf = None
        else:
            self.yDf = self.masterDf.groupby(self.groupByColumnName, as_index=False)[
                yStat
            ].agg(aggList)
            self.yDf = self.yDf.reset_index()
            self.yDf.insert(1, "stat", yStat)  # column 1, in place

        if xStat == yStat:
            groupList = [xStat]
        else:
            groupList = [xStat, yStat]
        if groupByNone:
            meanDf = None
        else:
            meanDf = self.masterDf.groupby(self.groupByColumnName, as_index=False)[
                groupList
            ].mean()
            meanDf = meanDf.reset_index()

        #
        # 20210211 get median/std/sem/n
        # try and add median/std/sem/sem/n
        if 0:
            tmpDf = self.masterDf.groupby(self.groupByColumnName, as_index=False)[
                groupList
            ].median()
            # print('tmpDf:', tmpDf)
            meanDf["median"] = tmpDf[self.groupByColumnName]

        """
        for catName in self.masterCatColumns:
            #if catName == 'analysisname':
            if catName == self.groupByColumnName:
                # this is column we grouped by, already in meanDf
                continue
            meanDf[catName] = ''
        """

        # for each row, update all categorical columns using self.masterCatColumns
        fileNameList = meanDf[self.groupByColumnName].unique()
        # print('  getMeanDf() updating all categorical columns for rows in fileNameList:')#, fileNameList)
        # print('    categorical columns are self.masterCatColumns:', self.masterCatColumns)
        for i, analysisname in enumerate(fileNameList):
            tmpDf = self.masterDf[self.masterDf[self.groupByColumnName] == analysisname]
            if len(tmpDf) == 0:
                print("  ERROR: got 0 length for analysisname:", analysisname)
                continue
            # need to limit this to pre-defined catecorical columns
            for catName in self.masterCatColumns:
                # if i==0:
                #    print('analysisname:', analysisname, 'catName:', catName)

                # if catName == self.groupByColumnName:
                #    # this is column we grouped by, already in meanDf
                #    continue

                # find value of catName column from 1st instance in masterDf
                # if more than one unique value, don't put into table
                numUnique = len(tmpDf[catName].unique())
                if numUnique == 1:
                    # print('    updating categorical colum with catName:', catName)
                    catValue = tmpDf[catName].iloc[0]

                    theseRows = (
                        meanDf[self.groupByColumnName] == analysisname
                    ).tolist()
                    # if catName == self.groupByColumnName:
                    meanDf.loc[theseRows, catName] = catValue
                    if self.xDf is not None:
                        self.xDf.loc[theseRows, catName] = catValue
                    if self.yDf is not None:
                        self.yDf.loc[theseRows, catName] = catValue
                    # print('catName:', catName, 'catValue:', type(catValue), catValue)
                else:
                    print(
                        "warning catName:", catName, "has", numUnique, "unique values"
                    )
                    pass
                    # print(f'not adding {catName} to table, numUnique: {numUnique}')
        #
        # sort
        # meanDf = meanDf.sort_values(['Region', 'Sex', 'Condition'])
        if self.sortOrder is not None:
            print("  ", "sorting by self.sortOrder:", self.sortOrder)
            try:
                meanDf = meanDf.sort_values(self.sortOrder)
                if self.xDf is not None:
                    self.xDf = self.xDf.sort_values(self.sortOrder)
                if self.yDf is not None:
                    self.yDf = self.yDf.sort_values(self.sortOrder)
            except KeyError as e:
                print("    ", "sorting (2) failed with:", e)
        #
        meanDf["index"] = [x + 1 for x in range(len(meanDf))]
        meanDf = meanDf.reset_index()
        if self.xDf is not None:
            self.xDf["index"] = [x + 1 for x in range(len(self.xDf))]
            self.xDf = self.xDf.reset_index()
        if self.yDf is not None:
            self.yDf["index"] = [x + 1 for x in range(len(self.yDf))]
            self.yDf = self.yDf.reset_index()

        # drop extra columns
        # modelMeanDf = meanDf.drop(['level_0'], axis=1)
        if self.xDf is not None and "level_0" in self.xDf.columns:
            self.xDf = self.xDf.drop(["level_0"], axis=1)
        if self.yDf is not None and "level_0" in self.yDf.columns:
            self.yDf = self.yDf.drop(["level_0"], axis=1)
        # we need to round for the table, but NOT the plot !!!!!
        # meanDf = meanDf.round(3)

        #
        if verbose:
            print("getMeanDf():")
            print(meanDf)
        #
        return meanDf

    def updateGlobal(self):
        """
        update all plots
        do not replot
        """
        state = self.getState()
        n = len(self.myPlotCanvasList)
        for i in range(n):
            if self.myPlotCanvasList[i] is not None:
                self.myPlotCanvasList[i].myUpdateGlobal(state)

    def update2(self):
        # self.updatePlot is the plot to update 1,2,3,4
        updateIndex = self.updatePlot
        if updateIndex is None:
            print(
                "update2() found no plots to update with self.updatePlot:",
                self.updatePlot,
            )
            self.mySetStatusBar("Please select a plot to update")
            return

        state = self.getState()

        # update table model
        meanDf = state["meanDf"]
        # modelMeanDf = meanDf.drop(['level_0'], axis=1)
        # self.myModel = myPandasModel(meanDf) # todo: don't need self.myModel
        myModel = myPandasModel(meanDf)  # todo: don't need self.myModel
        # print('calling _switchTableModel from update2() updateIndex:', updateIndex)
        self._switchTableModel(myModel)

        self.xTableView.slotSwitchTableDf(self.xDf)
        self.yTableView.slotSwitchTableDf(self.yDf)

        # update plots
        n = len(self.myPlotCanvasList)
        for i in range(n):
            if i == updateIndex:
                if self.myPlotCanvasList[i] is not None:
                    self.myPlotCanvasList[i].myUpdate(state)

    def old_getAnnotation(self, ind):
        # todo: replace with _getStatFromPlot

        if not np.issubdtype(ind, np.integer):
            print("getAnnotation() got bad ind:", ind, type(ind))
            return

        analysisName = self.plotDf.at[ind, self.groupByColumnName]
        index = self.plotDf.at[ind, "index"]
        try:
            region = self.plotDf.at[ind, "Region"]  # not all will have this
        except KeyError as e:
            region = "n/a"
        xVal = self.plotDf.at[ind, self.plotStatx]
        yVal = self.plotDf.at[ind, self.plotStaty]

        returnDict = {
            "index": index,
            "analysisName": analysisName,
            "region": region,
            "xVal": xVal,
            "yVal": yVal,
            #'plotDf': self.plotDf, # potentially very big
        }
        return returnDict

    # see: https://stackoverflow.com/questions/7908636/possible-to-make-labels-appear-when-hovering-over-a-point-in-matplotlib
    """
    def old_onHover(self, event):
        print('onHover:', type(event), 'inaxes:', event.inaxes)
        if event.inaxes == self.axes[0]:
            print('  in plotted axes')
        else:
            print('  not in plotted axes:', self.axes[0])

        print('  whatWeArePlotting:', type(self.whatWeArePlotting))
        cont, ind = self.whatWeArePlotting.contains(event)
        print('  cont:', cont)
        print('  ind:', ind)
        '''
        ind = event.ind # ind is a list []
        ind = ind[0]
        self._getStatFromPlot(ind)
        '''
    """

    '''
    def old_getStatFromPlot(self, ind):
        """
        get stat from self.plotDf from connected click/hover
        """
        analysisName = self.plotDf.at[ind, self.groupByColumnName]
        index = self.plotDf.at[ind, 'index']
        region = self.plotDf.at[ind, 'region']
        xVal = self.plotDf.at[ind, self.plotStatx]
        yVal = self.plotDf.at[ind, self.plotStaty]
        print(f'index:{index}, analysisName:{analysisName}, region:{region}, {self.plotStatx}:{xVal}, {self.plotStaty}:{yVal}')
    '''

    def slotSelectFromPlot(self, selectDict):
        """
        A point in one of plots was selected
        pass this to parent app
        """
        print("bScatterPlotMainWindow.slotSelectFromPlot() ", selectDict)

    def slotSelectSquare(self, plotNumber, stateDict):
        print("bScatterPlotMainWindow.slotSelectSquare()", plotNumber)
        # print('  stateDict:', stateDict)
        print("  todo: fill in all interface using stateDict")
        self.updatePlot = plotNumber

    def slotTableViewClicked(self, clickedIndex):
        """
        Respond to signal self.tableView.clicked
        clickedIndex: PyQt5.QtCore.QModelIndex
        """
        row = clickedIndex.row()
        model = clickedIndex.model()
        print(
            "bScatterPlotMainWindow.slotTableViewClicked() row:",
            row,
            "clickedIndex:",
            clickedIndex,
        )

        # select in plot
        # self._selectInd(row) # !!!! visually, index start at 1


if __name__ == "__main__":
    """
    20210112, extending this to work with any csv. Starting with nodes/edges from bimpy
    """

    # todo: using 'analysisname' for group by, I think I can also use 'File Number'
    statListDict = None  # list of dict mapping human readbale to column names
    masterDf = None
    interfaceDefaults = None

    # machine learning db
    if 0:
        # this is from mac laptop
        # path = '/Users/cudmore/data/laura-ephys/Superior vs Inferior database_master.csv'
        path = "/Users/cudmore/data/laura-ephys/SANdatabaseForMachineLearning.xlsx"
        analysisName = "File Number"
        # statListDict = None #sanpy.bAnalysisUtil.getStatList()
        categoricalList = ["LOCATION", "SEX", "File Number"]  # , 'File Name']
        hueTypes = ["LOCATION", "SEX", "File Number"]  # , 'File Name'] #, 'None']
        sortOrder = ["LOCATION", "SEX", "File Number"]

    # sanpy database
    if 1:
        # import sanpy
        # sys.path.append(os.path.join(os.path.dirname(sys.path[0]),'sanpy'))
        # import bAnalysisUtil
        # statListDict = bAnalysisUtil.statList
        import statlist

        statListDict = statlist.statList

        # this is from mac laptop
        # path = '/Users/cudmore/data/laura-ephys/Superior vs Inferior database_master.csv'
        path = "../examples/Superior vs Inferior database_master.csv"
        path = "/Users/cudmore/data/laura-ephys/Superior_Inferior_database_master_jan25.csv"
        path = (
            "/Users/cudmore/data/laura-ephys/Superior vs Inferior database_master.csv"
        )
        path = "/Users/cudmore/data/laura-ephys/Superior vs Inferior database_master_20210402.csv"

        # path = 'data/Superior vs Inferior database_master_20210402.csv'
        path = "data/Superior vs Inferior database_master_20210402.csv"
        # path = '/Users/cudmore/data/laura-ephys/Superior_Inferior_database_master_jan25.csv'
        path = "data/Superior vs Inferior database_13_Feb_master.csv"
        analysisName = "analysisname"
        # statListDict = None #sanpy.bAnalysisUtil.getStatList()
        categoricalList = [
            "include",
            "Condition",
            "Region",
            "Sex",
            "RegSex",
            "File Number",
            "analysisname",
        ]  # , 'File Name']
        hueTypes = [
            "Region",
            "Sex",
            "RegSex",
            "Condition",
            "File Number",
            "analysisname",
        ]  # , 'File Name'] #, 'None']
        sortOrder = ["Region", "Sex", "Condition"]

        interfaceDefaults = {
            "Y Statistic": "Spike Frequency (Hz)",
            "X Statistic": "Region",
            "Hue": "Region",
            "Group By": "File Number",
        }
    # bimpy database
    if 0:
        path = "../examples/edges_db.csv"
        analysisName = "fileNumber"
        categoricalList = ["san", "region", "path", "file", "fileNumber", "nCon"]
        hueTypes = categoricalList
        sortOrder = ["san", "region"]

    # dualAnalysis database
    if 0:
        # grab our list of dict mapping human readable to .csv column names
        sys.path.append(os.path.join(os.path.dirname(sys.path[0]), "sanpy"))
        import bAnalysisUtil

        statListDict = bAnalysisUtil.statList

        path = "/Users/cudmore/Sites/SanPy/examples/dual-analysis/dualAnalysis_final_db.csv"
        analysisName = "fileNumber"  # # rows in .xlsx database, one recording per row
        # trial is 1a/1b/1c... trial withing cellNumber
        categoricalList = [
            "include",
            "region",
            "fileNumber",
            "cellNumber",
            "trial",
            "quality",
        ]
        hueTypes = categoricalList
        sortOrder = ["region"]

    # sparkmaster lcr database
    if 0:
        path = "/Users/cudmore/Sites/SanPy/examples/dual-analysis/lcr-database.csv"
        analysisName = "fileNumber"  # # rows in .xlsx database, one recording per row
        # trial is 1a/1b/1c... trial withing cellNumber
        categoricalList = ["quality", "region", "fileNumber", "dateFolder", "tifFile"]
        hueTypes = categoricalList
        sortOrder = ["region"]

    # lcr/vm analysis using lcrPicker.py
    if 0:
        # basePath = '/Users/cudmore/Sites/SanPy/examples/dual-analysis/'
        # path = basePath + 'dual-data/20210115/20210115__0002_lcrPicker.csv'
        # path = basePath + 'dual-data/20210115/20210115__0001_lcrPicker.csv'

        # output of lcrPicker.py ... mergeDatabase()
        path = "/Users/cudmore/Sites/SanPy/examples/dual-analysis/lcrPicker-db.csv"
        categoricalList = None
        hueTypes = None
        analysisName = "tifFile"
        sortOrder = None

    # merged sanpy+lcr pre spike slope
    # generated by dualAnalysis.py xxx()
    # usnig to compare lcr slope to edddr for fig 9
    if 0:
        path = "/Users/cudmore/Sites/SanPy/examples/dual-analysis/combined-sanpy-lcr-db.csv"
        statListDict = None
        categoricalList = None
        hueTypes = None
        analysisName = "filename"
        sortOrder = None

    #
    app = QtWidgets.QApplication(sys.argv)

    ex = bScatterPlotMainWindow(
        path,
        categoricalList,
        hueTypes,
        analysisName,
        sortOrder,
        statListDict=statListDict,
        masterDf=masterDf,
        interfaceDefaults=interfaceDefaults,
    )

    sys.exit(app.exec_())
