#!python
# PYTHON_ARGCOMPLETE_OK
#
# Copyright (c) 2020 Björn Gottschall
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


import argparse
import argcomplete
import tempfile
import pandas
import numpy
import os
import io
import gc
import subprocess
import sys
import pickle
import copy
import textwrap
import shutil
import xopen
import seaborn

__prog__ = 'plotgen'
__version__ = '0.5.3'
__url__ = 'https://github.com/bgottschall/plotgen'


# All options that are forwared to the plotting script will be filtered through here
def escapedStr(val: str):
    return val.translate(str.maketrans({
        "'": r"\'",
        "\\": r"\\",
        "\"": r"\"",
    }))


def isFloat(val):
    if val is None:
        return False
    try:
        float(val)
        return True
    except ValueError:
        return False


class OrderedAction(argparse.Action):
    def __init__(self, *args, ordered=True, sub_action='store', **kwargs):
        super().__init__(*args, **kwargs)
        self.action = sub_action
        self.ordered = ordered

    def __call__(self, parser, namespace, values, option_string=None):
        _action = parser._registry_get('action', self.action, self.action)(self.option_strings, self.dest)
        _action(parser, namespace, values, option_string)
        if 'ordered_args' not in namespace:
            setattr(namespace, 'ordered_args', [])
        if self.ordered:
            previous = namespace.ordered_args
            if (self.action == 'append'):
                for i, (k, v) in enumerate(previous):
                    if k == self.dest:
                        previous[i] = (k, getattr(namespace, self.dest))
                        break
            else:
                previous.append((self.dest, getattr(namespace, self.dest)))
            setattr(namespace, 'ordered_args', previous)


class ParentAction(argparse.Action):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, default=[], **kwargs)
        self.children = []

    def __call__(self, parser, namespace, values, option_string=None):
        items = getattr(namespace, self.dest)
        nspace = type(namespace)()
        for child in self.children:
            if (not child.sticky_default and child.name in ChildAction._adjusting_defaults):
                setattr(nspace, child.name, ChildAction._adjusting_defaults[child.name])
            else:
                setattr(nspace, child.name, child.default)
            setattr(nspace, 'ordered_args', [])
        items.append({'value': values, 'args': nspace})


class ChildAction(argparse.Action):
    _adjusting_defaults = {}

    def __init__(self, *args, parent, sub_action='store', sticky_default=False, ordered=True, **kwargs):
        super().__init__(*args, **kwargs)
        self.dest, self.name = parent.dest, self.dest
        self.sticky_default = sticky_default

        self.sub_action = sub_action
        self.action = OrderedAction
        self.ordered = ordered

        self.parent = parent
        parent.children.append(self)

    def __call__(self, parser, namespace, values, option_string=None):
        ChildAction._adjusting_defaults[self.name] = True if self.action == 'store_true' else values
        items = getattr(namespace, self.dest)
        try:
            lastParentNamespace = items[-1]['args']
        except Exception:
            if (self.sticky_default):
                raise Exception(f'parameter --{self.name} can only be used after --{self.parent.dest}!') from None
                exit(1)
            return
        _action = parser._registry_get('action', self.action, self.action)(self.option_strings, self.name, sub_action=self.sub_action, ordered=self.ordered)
        _action(parser, lastParentNamespace, values, option_string)


class Range(object):
    def __init__(self, start=None, end=None, orValues=None, start_inclusive=True, end_inclusive=True):
        if (start is not None and not isFloat(start)) or (end is not None and not isFloat(end)) or (orValues is not None and not isinstance(orValues, list)):
            raise Exception('invalid use of range object!')
        self.start_inclusive = start_inclusive
        self.end_inclusive = end_inclusive
        self.start = start
        self.end = end
        self.orValues = orValues

    def __eq__(self, other):
        ret = False
        if isFloat(other):
            other = float(other)
            if self.start is None and self.end is None:
                ret = True
            elif self.start is not None and self.end is not None:
                ret = (self.start <= other if self.start_inclusive else self.start < other) and (other <= self.end if self.end_inclusive else other < self.end)
            elif self.start is not None:
                ret = (self.start <= other if self.start_inclusive else self.start < other)
            elif self.end is not None:
                ret = (other <= self.end if self.end_inclusive else other < self.end)
        if not ret and self.orValues is not None:
            ret = other in self.orValues
        return ret

    def __contains__(self, item):
        return self.__eq__(item)

    def __iter__(self):
        yield self

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        if self.start is None and self.end is None:
            ret = '-inf - +inf'
        elif self.start is not None and self.end is not None:
            ret = f'{self.start} - {self.end}'
        elif (self.start is not None):
            ret = f'{self.start} - +inf'
        else:
            ret = f'-inf - {self.end}'
        return ret + (', or ' + ', '.join(self.orValues) if self.orValues is not None and len(self.orValues) > 0 else '')


defaultSliceTypeTranslator = {'all': slice(None)}


def SliceType(translator=defaultSliceTypeTranslator):
    def str2slice(value):
        if value in translator:
            return translator[value]
        try:
            return int(value)
        except ValueError:
            tSection = [int(s) if s else None for s in value.split(':')]
            if len(tSection) > 3:
                raise ValueError(f'{value} is not a valid slice notation')
            return slice(*tSection)
    return str2slice


def isSliceType(value, translator=defaultSliceTypeTranslator):
    if value is None:
        return False
    try:
        SliceType(translator)(value)
        return True
    except Exception:
        return False


def getSliceTypeIds(targetRange: range, slices: list):
    validIntRange = range(-len(targetRange), len(targetRange))
    selectedRanges = [targetRange[s] if isinstance(s, slice) else [targetRange[s]] if s in validIntRange else [] for s in slices]
    return [i for li in selectedRanges for i in li]


def updateRange(_range, dataList):
    if not isinstance(_range, list):
        raise Exception('updateRange needs a mutable list of min/max directories')
    for a in _range:
        if not isinstance(a, dict):
            raise Exception('updateRange needs a mutable list of directories')
        if 'min' not in a:
            a['min'] = None
        if 'max' not in a:
            a['max'] = None
    while len(_range) < len(dataList):
        _range.extend([{'min': None, 'max': None}])
    for index, data in enumerate(dataList):
        if data is not None:
            if not isinstance(data, list):
                data = [data]
            scope = [float(x) for x in data if isFloat(x)]
            if len(scope) > 0:
                _range[index]['min'] = min(scope) if _range[index]['min'] is None else min(_range[index]['min'], min(scope))
                _range[index]['max'] = max(scope) if _range[index]['max'] is None else max(_range[index]['max'], max(scope))


class DataframeActions:
    def filterFunction(frame, data, mode):
        filterMap = {
            '=': lambda x: any(x == y for y in data),
            '<': lambda x: any(x < y for y in data),
            '>': lambda x: any(x > y for y in data),
            '!=': lambda x: all(x != y for y in data),
            '<=': lambda x: any(x <= y for y in data),
            '>=': lambda x: any(x >= y for y in data)
        }

        if mode not in filterMap:
            return frame
        else:
            return frame.apply(filterMap[mode])

    def transpose(dataframe):
        return dataframe.transpose()

    def dropNaN(dataframe, dropAny=False):
        return dataframe.dropna(how='any' if dropAny else 'all', axis=0).dropna(how='any' if dropAny else 'all', axis=1)

    def sliceToColumnIds(dataframe, slices):
        if not isinstance(slices, list):
            slices = [slices]
        return getSliceTypeIds(range(dataframe.shape[1]), slices)

    def sliceToRowIds(dataframe, slices):
        if not isinstance(slices, list):
            slices = [slices]
        return getSliceTypeIds(range(dataframe.shape[0]), slices)

    def dropColumnsByIds(dataframe, colIds):
        filterColumns = numpy.array([False if i in colIds else True for i in range(dataframe.shape[1])])
        return dataframe.loc[:, filterColumns]

    def selectColumnsByIds(dataframe, colIds):
        return dataframe.iloc[:, colIds]

    def dropRowsByIds(dataframe, rowIds):
        filterRows = numpy.array([False if i in rowIds else True for i in range(dataframe.shape[0])])
        return dataframe.iloc[filterRows, :]

    def selectRowsByIds(dataframe, rowIds):
        return dataframe.iloc[rowIds, :]

    def filterRowsByColumnData(dataframe, columnIds, data, mode='='):
        if not isinstance(data, list):
            data = [data]
        data = [float(x) if isFloat(x) else x for x in data]
        for columnId in columnIds:
            dataframe = dataframe[DataframeActions.filterFunction(dataframe.iloc[:, columnId], data, mode)]
        return dataframe

    def filterColumnsByRowData(dataframe, rowIds, data, mode='='):
        if not isinstance(data, list):
            data = [data]
        data = [float(x) if isFloat(x) else x for x in data]
        for rowId in rowIds:
            dataframe = dataframe.loc[:, DataframeActions.filterFunction(dataframe.iloc[rowId, :], data, mode)]
        return dataframe

    def getColumnIds(dataframe, columns, mode='all', ignore_errors=False):
        if not isinstance(columns, list):
            columns = [columns]
        columnIds = []
        for col in columns:
            if col not in [str(x) for x in dataframe.columns]:
                isSlice = True
                try:
                    s = SliceType()(col)
                    columnIds.extend(DataframeActions.sliceToColumnIds(dataframe, s))
                except Exception:
                    isSlice = False
                if not isSlice and not ignore_errors:
                    raise Exception(f'Could not find column name {col}')
            else:
                selection = reversed(list(enumerate(dataframe.columns.tolist()))) if mode == 'last' else enumerate(dataframe.columns.tolist())
                for i, fcol in selection:
                    if str(fcol) == col:
                        columnIds.append(i)
                        if mode != 'all':
                            break
        return columnIds

    def getRowIds(dataframe, rows, mode='all', ignore_errors=False):
        if not isinstance(rows, list):
            rows = [rows]
        rowIds = []
        for row in rows:
            if row not in [str(x) for x in dataframe.index]:
                isSlice = True
                try:
                    s = SliceType()(row)
                    rowIds.extend(DataframeActions.sliceToRowIds(dataframe, s))
                except Exception:
                    isSlice = False
                if not isSlice and not ignore_errors:
                    raise Exception(f'Could not find row name {row}')
            else:
                selection = reversed(list(enumerate(dataframe.index.tolist()))) if mode == 'last' else enumerate(dataframe.index.tolist())
                for i, frow in selection:
                    if str(frow) == row:
                        rowIds.append(i)
                        if mode != 'all':
                            break
        return rowIds

    def resetIndex(dataframe):
        return dataframe.reset_index()

    def setIndexColumnByIdx(dataframe, colIdx):
        return dataframe.set_index(dataframe.iloc[:, colIdx])

    def renameColumns(dataframe, names):
        if not isinstance(names, list):
            names = [names]
        names = [float(x) if isFloat(x) else x for x in names]
        dataframe.columns = (names + dataframe.columns.to_list()[len(names):])[:len(dataframe.columns)]
        return dataframe

    def renameRows(dataframe, names):
        if not isinstance(names, list):
            names = [names]
        names = [float(x) if isFloat(x) else x for x in names]
        dataframe.index = (names + dataframe.index.to_list()[len(names):])[:len(dataframe.index)]
        return dataframe

    def sortColumns(dataframe, function='mean', order='asc'):
        sortKey = getattr(dataframe, function)(axis=0)
        sortKey.reset_index(drop=True, inplace=True)
        return dataframe.iloc[:, sortKey.sort_values(ascending=(order == 'asc')).index]

    def sortRows(dataframe, function='mean', order='asc'):
        sortKey = getattr(dataframe, function)(axis=1)
        sortKey.reset_index(drop=True, inplace=True)
        return dataframe.iloc[sortKey.sort_values(ascending=(order == 'asc')).index, :]

    def reverseColumns(dataframe):
        return dataframe.iloc[::, ::-1]

    def reverseRows(dataframe):
        return dataframe.iloc[::-1]

    def sortColumnsByRowIds(dataframe, rowIds, function='mean', order='asc', quiet=False):
        if not isinstance(rowIds, list):
            rowIds = [rowIds]
        if 'columns' in rowIds:
            sortKey = dataframe.columns.to_series()
        else:
            sortKey = getattr(dataframe.iloc[rowIds], function)(axis=0)
        sortKey.reset_index(drop=True, inplace=True)
        return dataframe.iloc[:, sortKey.sort_values(ascending=(order == 'asc')).index]

    def sortRowsByColumnIds(dataframe, colIds, function='mean', order='asc', quiet=False):
        if not isinstance(colIds, list):
            colIds = [colIds]
        if 'index' in colIds:
            sortKey = pandas.to_numeric(dataframe.index, errors="ignore").to_series()
        else:
            sortKey = getattr(dataframe.iloc[:, colIds], function)(axis=1)
        sortKey.reset_index(drop=True, inplace=True)
        return dataframe.iloc[sortKey.sort_values(ascending=(order == 'asc')).index, :]

    def addConstant(dataframe, constant):
        return dataframe + constant

    def scaleConstant(dataframe, constant):
        return dataframe * constant

    def normaliseToConstant(dataframe, constant):
        return dataframe / constant

    def abs(dataframe):
        return dataframe.abs()

    def applyOnRows(dataframe, applyRowIds, targetRowIds=[], function='abs', parameter='1', applyMode='normal', quiet=False):
        if not isinstance(applyRowIds, list):
            applyRowIds = [applyRowIds]
        if not isinstance(targetRowIds, list):
            targetRowIds = [targetRowIds]

        parameter = int(parameter) if str(parameter).isdigit() else float(parameter) if isFloat(parameter) else parameter

        colWarning = False
        constMap = {
            'zero': 0,
            'one': 1,
            'nan': numpy.nan,
            'set': parameter
        }

        setMap = {
            'setzero': 0,
            'setone': 1,
            'setnan': numpy.nan,
        }

        # These functions are applied on each row individually, targetRowIds will be considered as applyRowIds as well
        pandasSelfFunctions = ['abs', 'cumsum', 'cummax', 'cummin', 'cumprod', 'rank']
        specialSelfFunctions = list(setMap.keys()) + list(constMap.keys()) + ['polyfit']

        with pandas.option_context('mode.chained_assignment', None):
            if function in (pandasSelfFunctions + specialSelfFunctions):
                applyRowIds += targetRowIds

                if function in pandasSelfFunctions:
                    if function == 'abs':
                        dataframe.iloc[applyRowIds, :] = dataframe.iloc[applyRowIds, :].abs().values
                    else:
                        dataframe.iloc[applyRowIds, :] = getattr(dataframe.iloc[applyRowIds, :], function)(axis=1).values
                elif function in constMap:
                    dataframe.iloc[applyRowIds, :] = constMap[function]
                elif function in setMap:
                    dataframe.iloc[applyRowIds, :] = dataframe.iloc[applyRowIds, :].applymap(lambda x: setMap[function] if x == parameter else x)
                elif function == 'polyfit':
                    try:
                        parameter = int(parameter)
                    except Exception:
                        raise 'ERROR: apply parameter needs to be an integer for polyfit'
                    for rowIdx in applyRowIds:
                        fitTarget = dataframe.iloc[rowIdx, :].apply(pandas.to_numeric, errors='coerce')
                        if not quiet and fitTarget.isna().values.any():
                            print('WARNING: NaN values are replaced with zero for polynomial fitting!', file=sys.stderr)
                        fitTarget = fitTarget.fillna(0).to_list()
                        fitAlong = dataframe.columns.to_list()
                        if not all([isFloat(x) for x in fitAlong]):
                            if not quiet and not colWarning:
                                colWarning = True
                                print('WARNING: columns are not numeric, will fit along a static number series!', file=sys.stderr)
                            fitAlong = list(range(len(fitTarget)))
                        dataframe.iloc[rowIdx, :] = numpy.polyval(numpy.polyfit(fitAlong, fitTarget, parameter), fitAlong)

            else:
                # All other functions are computations between applyRowIds and targetRowIds
                if len(targetRowIds) == 0:
                    # If targetRowIds is empty, fill it with all rows not in applyRowIds
                    targetRowIds = list(range(dataframe.shape[0]))
                    targetRowIds = [r for r in targetRowIds if r not in applyRowIds]

                if len(targetRowIds) > 0:
                    if applyMode == 'reverse':
                        targetRowIds, applyRowIds = applyRowIds, targetRowIds

                    if function == 'copy':
                        if len(applyRowIds) > 0 and not args.quiet:
                            print('WARNING: only the last row has an effect with apply function set!', file=sys.stderr)
                        dataframe.iloc[targetRowIds, :] = dataframe.iloc[targetRowIds, :].apply(lambda _: dataframe.iloc[applyRowIds[-1], :], axis=1).values
                    else:
                        applyRows = [dataframe.iloc[rowIdx, :].apply(pandas.to_numeric, errors='coerce') for rowIdx in applyRowIds]
                        for applyRow in applyRows:
                            dataframe.iloc[targetRowIds, :] = getattr(dataframe.iloc[targetRowIds, :].apply(pandas.to_numeric, errors="coerce"), function)(applyRow, axis=1).values
        return dataframe

    def applyOnColumns(dataframe, applyColumnIds, targetColumnIds=[], function='abs', parameter='1', applyMode='normal', quiet=False):
        if not isinstance(applyColumnIds, list):
            applyColumnIds = [applyColumnIds]
        if not isinstance(targetColumnIds, list):
            targetColumnIds = [targetColumnIds]

        parameter = int(parameter) if str(parameter).isdigit() else float(parameter) if isFloat(parameter) else parameter

        indexWarning = False
        constMap = {
            'zero': 0,
            'one': 1,
            'nan': numpy.nan,
            'set': parameter
        }

        setMap = {
            'setzero': 0,
            'setone': 1,
            'setnan': numpy.nan,
        }
        # These functions are applied on each row individually, targetRowIds will be considered as applyRowIds as well
        pandasSelfFunctions = ['abs', 'cumsum', 'cummax', 'cummin', 'cumprod', 'rank', 'nan']
        specialSelfFunctions = list(setMap.keys()) + list(constMap.keys()) + ['polyfit']

        with pandas.option_context('mode.chained_assignment', None):
            if function in (pandasSelfFunctions + specialSelfFunctions):
                applyColumnIds += targetColumnIds

                if function in pandasSelfFunctions:
                    if function == 'abs':
                        dataframe.iloc[:, applyColumnIds] = dataframe.iloc[:, applyColumnIds].abs().values
                    else:
                        dataframe.iloc[:, applyColumnIds] = getattr(dataframe.iloc[:, applyColumnIds], function)(axis=0).values
                elif function in constMap:
                    dataframe.iloc[:, applyColumnIds] = constMap[function]
                elif function in setMap:
                    dataframe.iloc[:, applyColumnIds] = dataframe.iloc[:, applyColumnIds].applymap(lambda x: setMap[function] if x == parameter else x)
                elif function == 'polyfit':
                    try:
                        parameter = int(parameter)
                    except Exception:
                        raise 'ERROR: apply parameter needs to be an integer for polyfit'
                    for columnIdx in applyColumnIds:
                        fitTarget = dataframe.iloc[:, columnIdx].apply(pandas.to_numeric, errors='coerce')
                        if not quiet and fitTarget.isna().values.any():
                            print('WARNING: NaN values are replaced with zero for polynomial fitting!', file=sys.stderr)
                        fitTarget = fitTarget.fillna(0).to_list()
                        fitAlong = dataframe.index.to_list()
                        if not all([isFloat(x) for x in fitAlong]):
                            if not quiet and not indexWarning:
                                indexWarning = True
                                print('WARNING: index is not numeric, will fit along a static number series!', file=sys.stderr)
                            fitAlong = list(range(len(fitTarget)))
                        dataframe.iloc[:, columnIdx] = numpy.polyval(numpy.polyfit(fitAlong, fitTarget, parameter), fitAlong)

            else:
                # All other functions are computations between applyColumnIds and targetColumnIds
                if len(targetColumnIds) == 0:
                    # If targetColumnIds is empty, fill it with all columns not in applyColumnIds
                    targetColumnIds = list(range(dataframe.shape[1]))
                    targetColumnIds = [r for r in targetColumnIds if r not in applyColumnIds]

                if len(targetColumnIds) > 0:
                    if applyMode == 'reverse':
                        targetColumnIds, applyColumnIds = applyColumnIds, targetColumnIds

                    if function == 'copy':
                        if len(applyColumnIds) > 0 and not args.quiet:
                            print('WARNING: only the last column has an effect with apply function copy!', file=sys.stderr)
                        dataframe.iloc[:, targetColumnIds] = dataframe.iloc[:, targetColumnIds].apply(lambda _: dataframe.iloc[:, applyColumnIds[-1]], axis=0).values
                    else:
                        applyColumns = [dataframe.iloc[:, columnIdx].apply(pandas.to_numeric, errors='coerce') for columnIdx in applyColumnIds]
                        for applyColumn in applyColumns:
                            dataframe.iloc[:, targetColumnIds] = getattr(dataframe.iloc[:, targetColumnIds].apply(pandas.to_numeric, errors="coerce"), function)(applyColumn, axis=0).values
        return dataframe

    def addRow(dataframe, name, function='mean', where='back'):
        if function in ['nan', 'zero', 'one']:
            element = numpy.nan if function == 'nan' else 0 if function == 'zero' else 1
            newRow = pandas.DataFrame([[element] * dataframe.shape[1]], index=[name], columns=dataframe.columns)
        else:
            newRow = getattr(dataframe.apply(pandas.to_numeric, errors='coerce'), function)(axis=0)
            newRow = newRow.to_frame(name).transpose()
        return pandas.concat([dataframe, newRow], axis=0) if where == 'back' else pandas.concat([newRow, dataframe], axis=0)

    def addColumn(dataframe, name, function='mean', where='back'):
        if function in ['nan', 'zero', 'one']:
            element = numpy.nan if function == 'nan' else 0 if function == 'zero' else 1
            newCol = pandas.Series(data=[element] * dataframe.shape[0], name=name, index=dataframe.index)
        else:
            newCol = getattr(dataframe.apply(pandas.to_numeric, errors='coerce'), function)(axis=1)
            newCol.name = name
        return pandas.concat([dataframe, newCol], axis=1) if where == 'back' else pandas.concat([newCol, dataframe], axis=1)

    def groupByColumnIds(dataframe, columnIds, function='sum'):
        if not isinstance(columnIds, list):
            columnIds = [columnIds]
        if 'index' in columnIds:
            return getattr(dataframe.groupby(dataframe.index, axis=0), function)()
        else:
            grouper = [dataframe.columns[c] for c in columnIds]
            return getattr(dataframe.groupby(grouper, as_index=True, axis=0), function)()

    def groupByRowIds(dataframe, rowIds, function='sum'):
        if not isinstance(rowIds, list):
            rowIds = [rowIds]
        if 'columns' in rowIds:
            return getattr(dataframe.groupby(dataframe.columns, axis=1), function)()
        else:
            grouper = [dataframe.index[r] for r in rowIds]
            print(grouper)
            return getattr(dataframe.groupby(grouper, axis=1), function)()

    def joinFrames(dataframes, function='index'):
        joinedFrame = None
        for frame in dataframes:
            joinedFrame = frame if joinedFrame is None else pandas.concat([joinedFrame, frame], axis=(1 if function == 'index' else 0), join='outer', verify_integrity=False, copy=True)
        return joinedFrame

    def splitFramesByRowIdx(dataframes, rowIdx):
        newFrames = []
        for frame in dataframes:
            if rowIdx == 'columns':
                for v in frame.columns.unique():
                    columnIds = DataframeActions.getColumnIds(frame, v, 'all', False)
                    newFrames.append(DataframeActions.selectColumnsByIds(frame, columnIds))
            else:
                rowIdx = int(rowIdx)
                for v in frame.iloc[rowIdx, :].unique():
                    if v != v:
                        newFrames.append(frame[frame.iloc[rowIdx, :].isna()])
                    else:
                        newFrames.append(frame[frame.iloc[rowIdx, :] == v])
        return newFrames

    def splitFramesByColumnIdx(dataframes, columnIdx):
        newFrames = []
        for frame in dataframes:
            if columnIdx == 'index':
                for v in frame.index.unique():
                    rowIds = DataframeActions.getRowIds(frame, v, 'all', False)
                    newFrames.append(DataframeActions.selectRowsByIds(frame, rowIds))
            else:
                columnIdx = int(columnIdx)
                for v in frame.iloc[:, columnIdx].unique():
                    if v != v:
                        newFrames.append(frame[frame.iloc[:, columnIdx].isna()])
                    else:
                        newFrames.append(frame[frame.iloc[:, columnIdx] == v])
        return newFrames

    def printFrames(filenames, dataframe, frameIndex, frameCount, precision=None):
        if not isinstance(filenames, list):
            filenames = [filenames]
        consoleWidth = shutil.get_terminal_size((80, 40))
        pSep = '---'
        if len(filenames) > 0:
            pFiles = f"File: {', '.join(filenames)}"
            pSep = '-' * min(consoleWidth.columns, len(pFiles))
        print(pSep + f'\nFrame: {frameIndex+1}/{frameCount}')
        if len(filenames) > 0:
            print(textwrap.fill(pFiles, width=consoleWidth.columns, subsequent_indent=' '))
        print(pSep)
        with pandas.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', consoleWidth.columns, 'display.max_columns', None, 'display.float_format', None if precision is None else f'{{:.{precision}f}}'.format):
            print(dataframe)
        print(pSep)

    def framesToCSV(dataframes, filenames=['stdout'], separator=None, quiet=False, precision=None):
        for _index, (frame, filename) in enumerate(zip(dataframes, filenames)):
            sep = ';' if separator is None else separator
            if filename.endswith('.tsv'):
                sep = '\t'
            elif filename.endswith('.csv'):
                sep = ';'
            fFile = sys.stdout if filename == 'stdout' else sys.stderr if filename == 'stderr' else xopen.xopen(filename, 'w')
            frame.to_csv(fFile, sep=sep, na_rep='NaN', float_format=None if precision is None else f'%.{precision}f')
            if (fFile != sys.stdout and fFile != sys.stdout):
                fFile.close()
            if not quiet and not fFile == sys.stdout:
                print(f'Frame {_index + 1}/{len(dataframes)} saved to {filename}')

    def framesToPickle(dataframes, filename, quiet=False):
        fFile = sys.stdout.buffer if filename == 'stdout' else sys.stderr.buffer if filename == 'stderr' else xopen.xopen(filename, 'wb')
        pickle.dump(dataframes, fFile, pickle.HIGHEST_PROTOCOL)
        if not quiet and not fFile == sys.stdout.buffer:
            print(f'Dataframes saved to {filename}')
        if (fFile != sys.stdout.buffer and fFile != sys.stdout.buffer):
            fFile.close()


considerAsNaN = ['nan', 'none', 'null', 'zero', 'nodata', '']

traceSpecialColumns = ['error', 'error-', 'error+', 'offset', 'label', 'colour']
frameSpecialColumns = ['category']
allSpecialColumns = traceSpecialColumns + frameSpecialColumns

parser = argparse.ArgumentParser(description="Visualize your data the easy way")
# Global Arguments

parserFileOptions = parser.add_argument_group('file parsing options')

inputFileArgument = parser.add_argument('-i', '--input', type=str, help="input file to parse", nargs="+", action=ParentAction, required=True)
# Per File Parsing Arguments
parserFileOptions.add_argument("--special-column-prefix", help="special columns (or ignore columns) starting with (default %(default)s)", type=str, default='_', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--comment", help="ignores lines starting with (default %(default)s)", type=str, default='#', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--delimiter", help="data delimiter (auto detected by default)", type=str, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--no-columns", help="do not use a column row", default=False, sticky_default=True, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--no-index", help="do not use a index column", default=False, sticky_default=True, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)


parserFileOptions.add_argument("--index-icolumn", help="set index column after index", type=int, sticky_default=True, choices=Range(None, None), default=None, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--index-column", help="set index column", default=None, type=str, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--reset-index", help="reset index back into data frame as first column", default=False, nargs=0, sub_action="store_true", sticky_default=True, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--select-mode", help="select row/columns after policy (default %(default)s)", type=str, default='all', choices=['all', 'first', 'last'], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--ignore-icolumns", help="ignore these column indexes", type=SliceType(), default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--ignore-columns", help="ignore these columns", type=str, default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--ignore-irows", help="ignore these row indexes", type=SliceType(), default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--ignore-rows", help="ignore these rows", type=str, default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--select-irows", help="select these row indexes", type=SliceType(), default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--select-rows", help="select these rows", type=str, default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--select-icolumns", help="select these column indexes", type=SliceType(), default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--select-columns", help="select these columns", type=str, default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)


parserFileOptions.add_argument("--filter-mode", help="filter data mode (default %(default)s)", type=str, default="=", choices=['=', '!=', '<', '>', '<=', '>=' '!='], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--filter-irows", help="filter data from rows after indexes", type=str, default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--filter-row", help="filter data from rows", type=str, default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--filter-icolumns", help="filter data from columns after indexes", type=str, default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--filter-column", help="filter data from columns", type=str, default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--sort-order", help="sort rows after or column in this order (default %(default)s)", default='desc', choices=['asc', 'desc'], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--sort-function", help="sort rows after function or column (default %(default)s)", default='mean', choices=['mean', 'median', 'std', 'min', 'max'], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--sort-columns", help="sort columns", default=False, sub_action="store_true", nargs=0, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--sort-by-irows", help="sort column after this row index", type=SliceType({'columns': 'columns', **defaultSliceTypeTranslator}), default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--sort-by-rows", help="sort column after this row", type=str, default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--sort-rows", help="sort rows", default=False, sub_action="store_true", nargs=0, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--sort-by-icolumns", help="sort rows after this column index", type=SliceType({'index': 'index', **defaultSliceTypeTranslator}), default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--sort-by-columns", help="sort rows after this column", type=str, default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--reverse-columns", help="reverse columns order", default=False, sub_action="store_true", nargs=0, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--reverse-rows", help="reverse row order", default=False, sub_action="store_true", nargs=0, sticky_default=True, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--data-scale", help="scales data (default %(default)s)", type=float, default=1, choices=Range(None, None), sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--data-offset", help="offsets data (default %(default)s)", type=float, default=0, choices=Range(None, None), sticky_default=True, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--normalise-to", help="normalise data to (default %(default)s)", type=float, default=0, choices=Range(None, None), sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--normalise-to-icolumn", help="normalise to this column index", type=SliceType(), default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--normalise-to-column", help="normalise to this column", type=str, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--normalise-to-irow", help="normalise to this row index", type=SliceType(), default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--normalise-to-row", help="normalise to this row", type=str, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--add-at", help="add at the front or back of the dataframe", type=str, default='back', choices=['front', 'back'], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--add-function", help="use this function to compute new row/column", type=str, default='mean', choices=['sum', 'mean', 'median', 'std', 'var', 'sum', 'count', 'skew', 'mad', 'min', 'max', 'nan', 'zero', 'one'], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--add-column", help="add a new column with name", type=str, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--add-row", help="add a new row with name", type=str, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--group-function", help="use this function to compute grouped dataframe", type=str, default='sum', choices=['sum', 'mean', 'median', 'std', 'var', 'sum', 'count', 'skew', 'mad', 'min', 'max'], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--group-by-icolumns", help="group by this column index", type=SliceType({'index': 'index', **defaultSliceTypeTranslator}), default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--group-by-columns", help="group by this column", type=str, default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--group-by-irows", help="group by this row index", type=SliceType({'columns': 'columns', **defaultSliceTypeTranslator}), default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--group-by-rows", help="group by this row", type=str, default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--abs", help="convert all values to absolute values", type=str, default=False, nargs=0, sub_action="store_true", sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--apply-mode", help="apply first row/column parameter on all others or in reverse (default %(default)s)", type=str, default='normal', choices=['normal', 'reverse'], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--apply-parameter", help="additional function paramter (e.g. dimension of polyfit)", type=str, default='1', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--apply-function", help="use this function to compute new row/column", type=str, default='mean', choices=['add', 'radd', 'sub', 'rsub', 'mul', 'rmul', 'div', 'rdiv', 'mod', 'rmod', 'pow', 'rpow', 'cumsum', 'cummax', 'cummin', 'cumprod', 'rank', 'nan', 'zero', 'one', 'abs', 'set', 'copy', 'polyfit', 'setzero', 'setone', 'setnan'], action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--apply-icolumns", help="compute function on multiple column indexes", type=SliceType(), default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--apply-irows", help="compute function on multiple row indexes", type=SliceType(), default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--apply-columns", help="compute function on multiple columns", type=str, default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--apply-rows", help="compute function on multiple rows", type=str, default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--column-names", help="rename columns", type=str, sticky_default=True, default=[], nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--row-names", help="rename rows", type=str, sticky_default=True, default=[], nargs='+', action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--drop-nan", help="dropping rows/columns that are completely empty", sticky_default=True, default=False, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--drop-any-nan", help="dropping rows/columns that contain empty values", sticky_default=True, default=False, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--transpose", help="transpose data", default=False, sticky_default=True, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--print", help="print out each parsed input file", default=False, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--join", help="outer join input files on columns or index", default='none', choices=['none', 'index', 'columns'], sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--split-icolumn", help="split frame along this column index ('_index' splits by index)", type=str, sticky_default=True, choices=Range(None, None, ['index']), default=None, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--split-column", help="split frame along this column", type=str, sticky_default=True, default=None, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--split-irow", help="split frame along this row index ('_columns' splits by columns)", type=str, sticky_default=True, choices=Range(None, None, ['columns']), default=None, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--split-row", help="split frame along this row", type=str, sticky_default=True, default=None, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--focus-frames", help="set the frame focus for file options (default all)", type=SliceType(), default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--defocus-frames", help="remove frames from focus for file options", type=SliceType(), default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)

parserFileOptions.add_argument("--print-precision", help="set explicit output prevision for text and console output (default %(default)s)", type=str, default='default', choices=Range(0, None, ['default']), action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--file", help="save data frames to text files (one file per frame)", default=None, type=str, nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserFileOptions.add_argument("--pickle", help="pickle data frames to file (one file containing all frames)", default=None, type=str, sticky_default=True, action=ChildAction, parent=inputFileArgument)

# Per File Plotting Arguments:
parserPlotOptions = parser.add_argument_group('plot options')
parserPlotOptions.add_argument('--plot', choices=['line', 'bar', 'box', 'violin', 'heatmap'], help='plot type', default='line', action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--orientation", help="set plot orientation", default='auto', choices=['vertical', 'v', 'horizontal', 'h', 'auto'], action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--title", help="subplot title", type=escapedStr, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--use-name", help="use name for traces", type=escapedStr, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument('--row', type=int, choices=Range(1, None), help='subplot row (default %(default)s)', default=1, action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument('--rowspan', type=int, choices=Range(1, None), help='subplot rowspan (default %(default)s)', default=1, action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument('--col', type=int, choices=Range(1, None), help='subplot column (default %(default)s)', default=1, action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument('--colspan', type=int, choices=Range(1, None), help='subplot columnspan (default %(default)s)', default=1, action=ChildAction, parent=inputFileArgument)

parserPlotOptions.add_argument("--error", help="show error markers in plot (need to be supplied by data)", default='hide', choices=['show', 'hide'], action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--trace-names", help="set individual trace names", default=[], sticky_default=True, type=escapedStr, nargs='+', action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--trace-colours", help="define explicit trace colours", default=[], nargs='+', type=escapedStr, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--line-width", help="set line width (default %(default)s)", type=int, default=1, choices=Range(0,), action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--line-colour", help="set line colour  (default %(default)s) (line charts are using just colour)", type=escapedStr, default='#222222', action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--opacity", help="colour opacity (default 0.8 for overlay modes, else 1.0)", choices=Range(0, 1, ['auto']), action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--offsetgroups", help="set explicit offsetgroups for e.g. bar charts", type=int, default='auto', nargs='+', choices=Range(0, None, ['auto']), sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--legend-entries", help="choose which entries are shown in legend", choices=['all', 'unique', 'none'], default=None, action=ChildAction, parent=inputFileArgument)
parserPlotOptions.add_argument("--distribution-mode", help="define whether the input data is parsed as binned or aggregated data (e.g. histogramm data) for appropriate plots (box and violin)", type=str, choices=['normal', 'aggregated'], default='normal', action=ChildAction, parent=inputFileArgument)

parserLinePlotOptions = parser.add_argument_group('line plot options')
parserLinePlotOptions.add_argument("--line-mode", choices=['none', 'lines', 'markers', 'text', 'lines+markers', 'lines+text', 'markers+text', 'lines+markers+text'], help="choose linemode (default %(default)s)", default='lines', action=ChildAction, parent=inputFileArgument)
parserLinePlotOptions.add_argument('--line-fill', choices=['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx', 'toself', 'tonext'], help='fill line area (default %(default)s)', default='none', action=ChildAction, parent=inputFileArgument)
parserLinePlotOptions.add_argument('--line-stack', help='stack line input traces (default %(default)s)', default=False, sticky_default=True, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)
parserLinePlotOptions.add_argument('--line-shape', choices=['linear', 'spline', 'hv', 'vh', 'hvh', 'vhv'], help='choose line shape (default %(default)s)', default='linear', action=ChildAction, parent=inputFileArgument)
parserLinePlotOptions.add_argument('--line-dash', choices=['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'], help='choose line dash (default %(default)s)', default='solid', action=ChildAction, parent=inputFileArgument)
parserLinePlotOptions.add_argument('--line-markers', choices=['circle', 'circle-open', 'circle-dot', 'circle-open-dot', 'square', 'square-open', 'square-dot', 'square-open-dot', 'diamond', 'diamond-open', 'diamond-dot', 'diamond-open-dot', 'cross', 'cross-open', 'cross-dot', 'cross-open-dot', 'x', 'x-open', 'x-dot', 'x-open-dot', 'triangle-up', 'triangle-up-open', 'triangle-up-dot', 'triangle-up-open-dot', 'triangle-down', 'triangle-down-open', 'triangle-down-dot', 'triangle-down-open-dot', 'triangle-left', 'triangle-left-open', 'triangle-left-dot', 'triangle-left-open-dot', 'triangle-right', 'triangle-right-open', 'triangle-right-dot', 'triangle-right-open-dot', 'triangle-ne', 'triangle-ne-open', 'triangle-ne-dot', 'triangle-ne-open-dot', 'triangle-se', 'triangle-se-open', 'triangle-se-dot', 'triangle-se-open-dot', 'triangle-sw', 'triangle-sw-open', 'triangle-sw-dot', 'triangle-sw-open-dot', 'triangle-nw', 'triangle-nw-open', 'triangle-nw-dot', 'triangle-nw-open-dot', 'pentagon', 'pentagon-open', 'pentagon-dot', 'pentagon-open-dot', 'hexagon', 'hexagon-open', 'hexagon-dot', 'hexagon-open-dot', 'hexagon2', 'hexagon2-open', 'hexagon2-dot', 'hexagon2-open-dot', 'octagon', 'octagon-open', 'octagon-dot', 'octagon-open-dot', 'star', 'star-open', 'star-dot', 'star-open-dot', 'hexagram', 'hexagram-open', 'hexagram-dot', 'hexagram-open-dot', 'star-triangle-up', 'star-triangle-up-open', 'star-triangle-up-dot', 'star-triangle-up-open-dot', 'star-triangle-down', 'star-triangle-down-open', 'star-triangle-down-dot', 'star-triangle-down-open-dot', 'star-square', 'star-square-open', 'star-square-dot', 'star-square-open-dot', 'star-diamond', 'star-diamond-open', 'star-diamond-dot', 'star-diamond-open-dot', 'diamond-tall', 'diamond-tall-open', 'diamond-tall-dot', 'diamond-tall-open-dot', 'diamond-wide', 'diamond-wide-open', 'diamond-wide-dot', 'diamond-wide-open-dot', 'hourglass', 'hourglass-open', 'bowtie', 'bowtie-open', 'circle-cross', 'circle-cross-open', 'circle-x', 'circle-x-open', 'square-cross', 'square-cross-open', 'square-x', 'square-x-open', 'diamond-cross', 'diamond-cross-open', 'diamond-x', 'diamond-x-open', 'cross-thin', 'cross-thin-open', 'x-thin', 'x-thin-open', 'asterisk', 'asterisk-open', 'hash', 'hash-open', 'hash-dot', 'hash-open-dot', 'y-up', 'y-up-open', 'y-down', 'y-down-open', 'y-left', 'y-left-open', 'y-right', 'y-right-open', 'line-ew', 'line-ew-open', 'line-ns', 'line-ns-open', 'line-ne', 'line-ne-open', 'line-nw', 'line-nw-open'], help='choose line marker (default circle)', default=[], nargs='+', action=ChildAction, parent=inputFileArgument)
parserLinePlotOptions.add_argument('--line-marker-size', help='choose line marker size (default %(default)s)', type=int, default=6, choices=Range(0, None), action=ChildAction, parent=inputFileArgument)
parserLinePlotOptions.add_argument("--line-text-position", choices=["top left", "top center", "top right", "middle left", "middle center", "middle right", "bottom left", "bottom center", "bottom right"], help="choose line text positon (default %(default)s)", default='middle center', action=ChildAction, parent=inputFileArgument)

parserBarPlotOptions = parser.add_argument_group('bar plot options')
parserBarPlotOptions.add_argument("--bar-mode", help="choose barmode (default %(default)s)", choices=['stack', 'group', 'overlay', 'relative'], default='group')
parserBarPlotOptions.add_argument("--bar-width", help="set explicit bar width", choices=Range(0, None, ['auto']), default='auto', action=ChildAction, parent=inputFileArgument)
parserBarPlotOptions.add_argument("--bar-shift", help="set bar shift", choices=Range(None, None, ['auto']), default='auto', action=ChildAction, parent=inputFileArgument)
parserBarPlotOptions.add_argument("--bar-text-position", help="choose bar text position (default %(default)s)", choices=["inside", "outside", "auto", "none"], default='none', action=ChildAction, parent=inputFileArgument)
parserBarPlotOptions.add_argument("--bar-text-template", help="set bar text template (default %(default)s)", type=escapedStr, default='', action=ChildAction, parent=inputFileArgument)
parserBarPlotOptions.add_argument("--bar-gap", help="set bar gap (default $(default)s)", choices=Range(0, 1, ['auto']), default='auto')
parserBarPlotOptions.add_argument("--bar-group-gap", help="set bar group gap (default $(default)s)", choices=Range(0, 1), default=0)

parserViolinPlotOptions = parser.add_argument_group('violin plot options')
parserViolinPlotOptions.add_argument("--violin-mode", help="choose violinmode (default %(default)s)", choices=['overlay', 'group', 'halfoverlay', 'halfgroup', 'halfhalf', 'poshalfgroup', 'neghalfgroup', 'poshalf', 'neghalf'], default='overlay', action=ChildAction, parent=inputFileArgument)
parserViolinPlotOptions.add_argument("--violin-mean", help="choose violin mean (default %(default)s)", choices=['none', 'line', 'box'], default='none', action=ChildAction, parent=inputFileArgument)
parserViolinPlotOptions.add_argument("--violin-points", help="set points mode for (default %(default)s)", type=str, default='none', choices=['all', 'outliers', 'suspectedoutliers', 'none'], action=ChildAction, parent=inputFileArgument)
parserViolinPlotOptions.add_argument("--violin-jitter", help="set jitter for violin points (default %(default)s)", type=float, default=0, choices=Range(0, 1), action=ChildAction, parent=inputFileArgument)
parserViolinPlotOptions.add_argument("--violin-width", help="change violin widths (default %(default)s)", type=float, default=0, choices=Range(0,), action=ChildAction, parent=inputFileArgument)
parserViolinPlotOptions.add_argument("--violin-gap", help="gap between violins (default %(default)s) (not compatible with violin-width)", type=float, default=0.3, choices=Range(0, 1))
parserViolinPlotOptions.add_argument("--violin-group-gap", help="gap between violin groups (default %(default)s) (not compatible with violin-width)", type=float, default=0.3, choices=Range(0, 1))

parserBoxPlotOptions = parser.add_argument_group('box plot options')
parserBoxPlotOptions.add_argument("--box-mode", choices=['overlay', 'group'], help="choose boxmode (default %(default)s)", default='overlay')
parserBoxPlotOptions.add_argument("--box-mean", choices=['none', 'line', 'dot'], help="choose box mean (default %(default)s)", default='dot', action=ChildAction, parent=inputFileArgument)
parserBoxPlotOptions.add_argument("--box-width", help="box width (default %(default)s)", type=float, default=0, choices=Range(0,), action=ChildAction, parent=inputFileArgument)
parserBoxPlotOptions.add_argument("--box-gap", help="gap between boxes (default %(default)s) (not compatible with box-width)", type=float, default=0.3, choices=Range(0, 1))
parserBoxPlotOptions.add_argument("--box-group-gap", help="gap between box groups (default %(default)s) (not compatible with box-width)", type=float, default=0.3, choices=Range(0, 1))


parserPlotAxisOptions = parser.add_argument_group('plot axis options')
parserPlotAxisOptions.add_argument("--y-secondary", help="plot to secondary y-axis", default=False, sticky_default=True, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-title", help="y-axis title", default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-title", help="x-axis title", default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-title-standoff", help="added margin between tick labels and y-title in px", choices=Range(0,), default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-title-standoff", help="added margin between tick labels and x-title in px", choices=Range(0,), default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-type", help="choose type for y-axis (default %(default)s)", choices=['-', 'linear', 'log', 'date', 'category'], default='-', action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-type", help="choose type for x-axis (default %(default)s)", choices=['-', 'linear', 'log', 'date', 'category'], default='-', action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-hide", help="hide y-axis", default=False, sticky_default=True, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-hide", help="hide x-axis", default=False, sticky_default=True, nargs=0, sub_action="store_true", action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-range-mode", help="choose range mode for x-axis (default %(default)s)", choices=['normal', 'tozero', 'nonnegative'], default='normal', action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-range-mode", help="choose range mode for y-axis (default %(default)s)", choices=['normal', 'tozero', 'nonnegative'], default='normal', action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-range-from", help="y-axis start (default %(default)s)", default='auto', sticky_default=True, choices=Range(None, None, ['auto']), action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-range-from", help="x-axis start (default %(default)s)", default='auto', sticky_default=True, choices=Range(None, None, ['auto']), action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-range-to", help="y-axis end (default %(default)s)", default='auto', sticky_default=True, choices=Range(None, None, ['auto']), action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-range-to", help="x-axis start (default %(default)s)", default='auto', sticky_default=True, choices=Range(None, None, ['auto']), action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-tick-hide", help="set format of y-axis ticks", default=False, nargs=0, sub_action="store_true", sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-tick-hide", help="set format of y-axis ticks", default=False, nargs=0, sub_action="store_true", sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-tick-format", help="set format of y-axis ticks", default='', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-tick-format", help="set format of x-axis ticks", default='', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-tick-suffix", help="add suffix to y-axis ticks", default='', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-tick-suffix", help="add suffix to x-axis ticks", default='', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-tick-prefix", help="add prefix to y-axis ticks", default='', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-tick-prefix", help="add prefix to x-axis ticks", default='', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-ticks", help="how to draw y ticks (default '%(default)s')", choices=['', 'inside', 'outside'], default='', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-ticks", help="how to draw x ticks (default '%(default)s')", choices=['', 'inside', 'outside'], default='', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-tickmode", help="tick mode y-axis (default '%(default)s')", choices=['auto', 'linear', 'array'], default='auto', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-tickmode", help="tick mode x-axis (default '%(default)s')", choices=['auto', 'linear', 'array'], default='auto', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-nticks", help="number of ticks on y-axis (only tick mode auto) (default %(default)s)", choices=Range(0,), default=0, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-nticks", help="number of ticks on x-axis (only tick mode auto) (default %(default)s)", choices=Range(0,), default=0, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-tick0", help="first tick on y-axis (only tick mode linear) (default %(default)s)", type=escapedStr, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-tick0", help="first tick on x-axis (only tick mode linear) (default %(default)s)", type=escapedStr, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-dtick", help="tick step on y-axis (only tick mode linear) (default %(default)s)", type=escapedStr, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-dtick", help="tick step on x-axis (only tick mode linear) (default %(default)s)", type=escapedStr, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-tickvals", help="tick values on y-axis (only tick mode array) (default %(default)s)", default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-tickvals", help="tick values on x-axis (only tick mode array) (default %(default)s)", default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-ticktext", help="tick text on y-axis (only tick mode array) (default %(default)s)", default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-ticktext", help="tick text on x-axis (only tick mode array) (default %(default)s)", default=[], sticky_default=True, nargs='+', action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-tickangle", help="tick angle on y-axis (default %(default)s)", default='auto', sticky_default=True, choices=Range(None, None, ['auto']), action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-tickangle", help="tick angle on x-axis (default %(default)s)", default='auto', sticky_default=True, choices=Range(None, None, ['auto']), action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-grid-colour", help="set y-grid colour", type=escapedStr, default=None, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-grid-colour", help="set x-grid colour", type=escapedStr, default=None, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-colour", help="set y-axis colour", type=escapedStr, default=None, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-colour", help="set x-axis colour", type=escapedStr, default=None, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--y-line-width", help="set y-axis line width (default %(default)s)", type=float, choices=Range(0, None), default=0, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--x-line-width", help="set x-axis line width (default %(default)s)", type=float, choices=Range(0, None), default=0, action=ChildAction, parent=inputFileArgument)
parserPlotAxisOptions.add_argument("--grid-colour", help="set grid colour", type=escapedStr, default=None, action=ChildAction, parent=inputFileArgument)

parserColourOptions = parser.add_argument_group('colour options')
parserColourOptions.add_argument("--theme", help="theme to use (all colour options only apply to 'palette')", default='palette', choices=["palette", "plotly", "plotly_white", "plotly_dark", "ggplot2", "seaborn", "simple_white", "none"])
parserColourOptions.add_argument("--colours", help="define explicit colours (filled up by palette)", default=[], nargs='+', type=escapedStr)
parserColourOptions.add_argument("--palette", help="valid seaborn colour palette (default %(default)s)", type=str, default='ch:s=2.8,rot=0.1,d=0.85,l=0.15')
parserColourOptions.add_argument("--palette-opacity", help="palette colour opacity (default %(default)s)", type=float, choices=Range(0, 1.0), default=1.0)
parserColourOptions.add_argument("--palette-reverse", help="reverse colour palette", action="store_true", default=False)
parserColourOptions.add_argument("--palette-count", help="manually set the number of colours to generate from the palette", type=int, choices=Range(1, None), default=None)
parserColourOptions.add_argument("--palette-start", help="set the palette start index (default %(default)s)", type=int, default=0, choices=Range(0, None))
parserColourOptions.add_argument("--palette-cmap", help="how to generate continous colour maps (default %(default)s)", type=str, default='linear', choices=['linear', 'log', 'rlog'])
parserColourOptions.add_argument("--subplot-colours", help="specify explicit subplot colours (sets default colour cycle to subplot)", type=escapedStr, default=[], nargs='+', sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserColourOptions.add_argument("--subplot-palette", help="valid seaborn colour palette used for this subplot (sets default colour cycle to subplot)", type=str, default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserColourOptions.add_argument("--subplot-palette-opacity", help="subplot palette opacity", type=float, choices=Range(0, 1.0), default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserColourOptions.add_argument("--subplot-palette-reverse", help="reverse subplot colour palette", sub_action="store_true", default=False, nargs=0, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserColourOptions.add_argument("--subplot-palette-count", help="manually set the number of colours to generate from the subplot palette (set default colour cycle to subplot)", type=int, choices=Range(1, None), default=None, sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserColourOptions.add_argument("--subplot-palette-start", help="set the subplot palette start index (default 0)", type=int, default=None, choices=Range(0, None), sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserColourOptions.add_argument("--subplot-palette-cmap", help="how to generate continous colour maps (default linear)", type=str, default=None, choices=['linear', 'log', 'rlog'], sticky_default=True, action=ChildAction, parent=inputFileArgument)
parserColourOptions.add_argument("--colour-cycle", help="cycle through colours globally or per subplot (default global)", choices=['subplot', 'global'], default=None, action=ChildAction, parent=inputFileArgument)
parserColourOptions.add_argument("--colour-debug", help="print generated colour palettes", action="store_true", default=False)
parserColourOptions.add_argument("--colour-scale", help="show or hide continous colour scales (default %(default)s)", choices=['show', 'hide'], default='show', action=ChildAction, parent=inputFileArgument)

parserColourOptions.add_argument("--per-trace-colours", help="one colour for each trace (default)", action='store_true', default=False)
parserColourOptions.add_argument("--per-frame-colours", help="one colour for each dataframe", action='store_true', default=False)
parserColourOptions.add_argument("--per-input-colours", help="one colour for each input file", action='store_true', default=False)
parserColourOptions.add_argument("--font-colour", help="font colour (default %(default)s)", type=escapedStr, default='#000000')
parserColourOptions.add_argument("--background-colour", help="set background colour  (default 'rgba(255, 255, 255, 0)')", type=escapedStr, default=None)

parserPlotGlobalOptions = parser.add_argument_group('plot global options')

parserPlotGlobalOptions.add_argument("--master-title", help="plot master title", type=escapedStr, default=None)
parserPlotGlobalOptions.add_argument("--x-master-title", help="x-axis master title", type=escapedStr, default=None)
parserPlotGlobalOptions.add_argument("--y-master-title", help="y-axis master title", type=escapedStr, default=None)
parserPlotGlobalOptions.add_argument("--x-share", help="share subplot x-axis (default %(default)s)", default=False, action="store_true")
parserPlotGlobalOptions.add_argument("--y-share", help="share subplot y-axis (default %(default)s)", default=False, action="store_true")
parserPlotGlobalOptions.add_argument("--vertical-spacing", type=float, help="vertical spacing between subplots", default=None, choices=Range(0, 1))
parserPlotGlobalOptions.add_argument("--horizontal-spacing", type=float, help="horizontal spacing between subplots", default=None, choices=Range(0, 1))
parserPlotGlobalOptions.add_argument("--font-size", help="font size (default %(default)s)", type=int, default=12)
parserPlotGlobalOptions.add_argument("--font-family", help="font family (default %(default)s)", type=escapedStr, default='"Open Sans", verdana, arial, sans-serif')

parserPlotGlobalOptions.add_argument("--legend", help="quick setting the legend position (default %(default)s)", type=str, choices=['topright', 'topcenter', 'topleft', 'bottomright', 'bottomcenter', 'bottomleft', 'middleleft', 'center', 'middleright', 'belowleft', 'belowcenter', 'belowright', 'aboveleft', 'abovecenter', 'aboveright', 'righttop', 'rightmiddle', 'rightbottom', 'lefttop', 'leftmiddle', 'leftbottom'], default='righttop')
parserPlotGlobalOptions.add_argument("--legend-x", help="x legend position (-2 to 3)", type=float, choices=Range(-2, 3), default=None)
parserPlotGlobalOptions.add_argument("--legend-y", help="y legend position (-2 to 3)", type=float, choices=Range(-2, 3), default=None)
parserPlotGlobalOptions.add_argument("--legend-x-anchor", help="set legend xanchor", choices=['auto', 'left', 'center', 'right'], default=None)
parserPlotGlobalOptions.add_argument("--legend-y-anchor", help="set legend yanchor", choices=['auto', 'top', 'bottom', 'middle'], default=None)
parserPlotGlobalOptions.add_argument("--legend-hide", help="hides legend", default=None, action="store_true")
parserPlotGlobalOptions.add_argument("--legend-show", help="forces legend to show up", default=None, action="store_true")
parserPlotGlobalOptions.add_argument("--legend-vertical", help="horizontal legend", default=None, action="store_true")
parserPlotGlobalOptions.add_argument("--legend-horizontal", help="vertical legend", default=None, action="store_true")
parserPlotGlobalOptions.add_argument("--legend-order", help="trace order of legend entries (default grouped)", type=str, choices=['normal', 'reversed', 'grouped', 'grouped+reversed', 'reversed+grouped'], default='grouped')
parserPlotGlobalOptions.add_argument("--legend-groupgap", help="gap between legend groups (default %(default)s)", type=int, choices=Range(0, None), default=10)

parserPlotGlobalOptions.add_argument("--margins", help="sets all margins", type=int, choices=Range(0, None), default=None)
parserPlotGlobalOptions.add_argument("--margin-l", help="sets left margin", type=int, choices=Range(0, None), default=None)
parserPlotGlobalOptions.add_argument("--margin-r", help="sets right margin", type=int, choices=Range(0, None), default=None)
parserPlotGlobalOptions.add_argument("--margin-t", help="sets top margin", type=int, choices=Range(0, None), default=None)
parserPlotGlobalOptions.add_argument("--margin-b", help="sets bottom margin", type=int, choices=Range(0, None), default=None)
parserPlotGlobalOptions.add_argument("--margin-pad", help="sets padding", type=int, choices=Range(0, None), default=None)

parserPlotGlobalOptions.add_argument("--width", help="plot width", type=int, default=1000)
parserPlotGlobalOptions.add_argument("--height", help="plot height", type=int)

parserOutputOptions = parser.add_argument_group('output options')
parserOutputOptions.add_argument("--browser", help="open plot in the browser", default=False, action="store_true")
parserOutputOptions.add_argument("-o", "--output", help="export plot to file (html, pdf, svg, png, py, ...)", default=[], nargs='+')
parserOutputOptions.add_argument("-q", "--quiet", action="store_true", help="no warnings and don't open output file", default=False)
parserOutputOptions.add_argument("-v", "--version", action="version", version='%(prog)s ' + __version__ + ', available at ' + __url__)
parserOutputOptions.add_argument("--debug", action="store_true", help="output some debug informations", default=False)

argcomplete.autocomplete(parser)
args = parser.parse_args()

violinMode = 'overlay'
commentColour = ''
commentBackgroundColour = ''

if args.theme != 'palette':
    # We have chosen a theme, so just comment all colour settings out
    commentColour = '# '
    commentBackgroundColour = '' if args.background_colour else '# '
    # Better to show all legend entries now if not otherwise chosen

if not args.background_colour:
    args.background_colour = 'rgba(255, 255, 255, 0)'

if (not args.per_trace_colours and not args.per_frame_colours and not args.per_input_colours) or (args.per_trace_colours):
    args.per_trace_colours = True
    args.per_frame_colours, args.per_input_colours = False, False
elif (args.per_frame_colours):
    args.per_input_colours = False


for input in args.input:
    options = input['args']
    # options.ignore_icolumns = list(set(options.ignore_icolumns))
    # options.ignore_columns = list(set(options.ignore_columns))

    options.traceSpecialColumns = [options.special_column_prefix + x for x in traceSpecialColumns]
    options.frameSpecialColumns = [options.special_column_prefix + x for x in frameSpecialColumns]

    if (options.opacity == 'auto' and ((options.plot == 'box' and 'overlay' in args.box_mode) or
                                       (options.plot == 'violin' and 'overlay' in options.violin_mode) or
                                       (options.plot == 'bar' and 'overlay' in args.bar_mode))):
        options.opacity = 0.8
    elif options.opacity == 'auto':
        options.opacity = 1.0

    if options.orientation == 'auto':
        options.vertical = options.plot != 'line'
    elif options.orientation in ['vertical', 'v']:
        options.vertical = True
    else:
        options.vertical = False
    options.horizontal = not options.vertical

    if len(options.line_markers) == 0:
        options.line_markers = ['circle']

    if options.error == 'show':
        options.show_error = True
    else:
        options.show_error = False
    options.hide_error = not options.show_error

    if options.colour_cycle is None and (len(options.subplot_colours) > 0 or options.subplot_palette is not None or options.subplot_palette_count is not None):
        options.colour_cycle = 'subplot'

    if options.colour_cycle is None:
        options.colour_cycle = 'global'

    options.y_grid_colour = f"'{options.y_grid_colour}'" if options.y_grid_colour is not None else f"'{options.grid_colour}'" if options.grid_colour is not None else None
    options.x_grid_colour = f"'{options.x_grid_colour}'" if options.x_grid_colour is not None else f"'{options.grid_colour}'" if options.grid_colour is not None else None
    options.x_colour = f"'{options.y_colour}'" if options.y_colour is not None else None
    options.y_colour = f"'{options.x_colour}'" if options.x_colour is not None else None
    options.y_line_width_forced = 'y_line_width' in [i for (i, _) in options.ordered_args]
    options.x_line_width_forced = 'x_line_width' in [i for (i, _) in options.ordered_args]

    options.y_range_from = None if options.y_range_from == 'auto' else float(options.y_range_from)
    options.x_range_from = None if options.x_range_from == 'auto' else float(options.x_range_from)
    options.y_range_to = None if options.y_range_to == 'auto' else float(options.y_range_to)
    options.x_range_to = None if options.x_range_to == 'auto' else float(options.x_range_to)
    options.bar_width = None if options.bar_width == 'auto' else float(options.bar_width)
    options.bar_shift = None if options.bar_shift == 'auto' else float(options.bar_shift)
    options.y_tickangle = None if options.y_tickangle == 'auto' else float(options.y_tickangle)
    options.x_tickangle = None if options.x_tickangle == 'auto' else float(options.x_tickangle)

    if options.legend_entries is None:
        if args.theme != 'palette':
            options.legend_entries = 'all'
        else:
            options.legend_entries = 'unique'

    if options.plot != 'line':
        # If explicitly set the range-to the automatic ranging would start at the
        # min value which is confusing, set the from to 0 for all but line plots
        if options.y_range_to is not None and options.y_range_from is None:
            options.y_range_from = 0
        if options.x_range_to is not None and options.x_range_from is None:
            options.x_range_from = 0

    violinMode = options.violin_mode

args.bar_gap = None if args.bar_gap == 'auto' else float(args.bar_gap)
args.master_title = f"'{args.master_title}'" if args.master_title is not None else None
args.y_master_title = f"'{args.y_master_title}'" if args.y_master_title is not None else None
args.x_master_title = f"'{args.x_master_title}'" if args.x_master_title is not None else None

if (args.legend_show is not None or args.legend_hide is not None):
    args.legend_show = not args.legend_hide
    args.legend_hide = not args.legend_show

# Setting the legend orientation if it was explicitly set
if args.legend_vertical is not None or args.legend_horizontal is not None:
    args.legend_vertical = not args.legend_horizontal
    args.legend_horizontal = not args.legend_vertical

if args.legend is not None:
    # If not legend orientation is set, set the default depending on the position
    if args.legend_horizontal is None:
        if args.legend.startswith('top') or args.legend.startswith('bottom') or args.legend.startswith('above') or args.legend.startswith('below'):
            args.legend_horizontal = True
        else:
            args.legend_horizontal = False
    args.legend_vertical = not args.legend_horizontal

    if (args.legend_y_anchor is None):
        if args.legend.startswith('middle') or args.legend.endswith('middle') or args.legend == 'center':
            args.legend_y_anchor = 'middle'
        elif args.legend.startswith('top') or args.legend.startswith('below'):
            args.legend_y_anchor = 'top'
        elif args.legend.startswith('bottom') or args.legend.startswith('above'):
            args.legend_y_anchor = 'bottom'

    if (args.legend_x_anchor is None):
        if args.legend.endswith('center') or args.legend == 'center':
            args.legend_x_anchor = 'center'
        elif args.legend.endswith('right') or args.legend.startswith('left'):
            args.legend_x_anchor = 'right'
        elif args.legend.endswith('left') or args.legend.startswith('right'):
            args.legend_x_anchor = 'left'

    if (args.legend_y is None):
        if args.legend.startswith('middle') or args.legend.endswith('middle') or args.legend == 'center':
            args.legend_y = 0.5
        elif args.legend.startswith('top') or args.legend.endswith('top'):
            args.legend_y = 1.0
        elif args.legend.startswith('bottom') or args.legend.endswith('bottom'):
            args.legend_y = 0.0
        elif args.legend.startswith('above'):
            args.legend_y = 1.0
        elif args.legend.startswith('below'):
            args.legend_y = -0.05

    if (args.legend_x is None):
        if args.legend.endswith('center') or args.legend == 'center':
            args.legend_x = 0.5
        elif args.legend.endswith('left'):
            args.legend_x = 0.0
        elif args.legend.endswith('right'):
            args.legend_x = 1.0
        elif args.legend.startswith('right'):
            args.legend_x = 1.02
        elif args.legend.startswith('left'):
            args.legend_x = -0.05

totalTraceCount = 0
totalFrameCount = 0
totalInputCount = 0
subplotGrid = [{'min': 1, 'max': 1}, {'min': 1, 'max': 1}]
subplotGridDefinition = {}
data = []

# None means it will be set automatically to True/False
defaultBottomMargin = True if args.x_master_title is not None else None
defaultLeftMargin = True if args.y_master_title is not None else None
defaultRightMargin = None
# Those are never set automatically so either use True or False
defaultTopMargin = True if args.master_title is not None else None
defaultPadMargin = False

doneSomething = False

for input in args.input:
    inputOptions = input['args']
    inputFileNames = input['value']
    inputOptions.filenames = inputFileNames
    inputOptions.traceCount = 0
    inputOptions.frameCount = 0
    inputOptions.frameIndex = 0
    inputFrames = []
    for filename in inputFileNames:
        if not os.path.exists(filename):
            raise Exception(f'Could not find file {filename}!')

        if args.debug:
            print(f"DEBUG: reading in {filename}", file=sys.stderr)

        buf = xopen.xopen(filename, mode='rb').read()

        try:
            frame = pickle.loads(buf)
        except Exception:
            frame = None

        options = copy.deepcopy(inputOptions)
        options.filenames = [filename]

        if frame is not None:
            if not args.quiet and inputOptions.no_index:
                print(f"WARNING: ignoring --no-index for {filename}", file=sys.stderr)
            if not args.quiet and inputOptions.no_columns:
                print(f"WARNING: ignoring --no-columns for {filename}", file=sys.stderr)

            if (isinstance(frame, list)):
                for f in frame:
                    if (not isinstance(f, pandas.DataFrame)):
                        raise Exception(f'pickle file {filename} is not a list of pandas dataframes!')
                    inputFrames.append((copy.deepcopy(options), f))
            elif (not isinstance(frame, pandas.DataFrame)):
                raise Exception(f'pickle file {filename} is not a pandas data frame!')
            else:
                inputFrames.append((options, frame))
        else:
            # Do not use pandas own comment option, as we only want to exclude lines that start with a comment character to allow it in the data
            if len(options.comment) > 0:
                buf = b''.join([line for line in io.BytesIO(buf).readlines() if not line.decode().strip(' ').startswith(options.comment)])

            frame = pandas.read_csv(io.BytesIO(buf),
                                    sep=options.delimiter,
                                    header=None,
                                    index_col=0 if not options.no_index else None,
                                    engine='python')

            if (not options.no_index):
                frame.index.name = frame.iloc[0].name

            if (not options.no_columns):
                frame.columns = frame.iloc[0]
                # frame.columns.name = frame.iloc[0].name
                frame = DataframeActions.dropRowsByIds(frame, [0])

            frame = frame.apply(pandas.to_numeric, errors='ignore')
            frame = frame.applymap(lambda x: float(x) if isFloat(x) else x)
            frame = frame.applymap(lambda x: numpy.nan if isinstance(x, str) and x.lower() in considerAsNaN else x)

            options.frameCount = 1
            options.frameIndex = inputOptions.frameCount
            frame = frame.fillna(value=numpy.nan)
            inputFrames.append((options, frame))
            inputOptions.frameCount += 1

        del buf
        gc.collect()
        if args.debug:
            print("DEBUG: done reading file", file=sys.stderr)

    selectMode = 'all'
    filterMode = '='
    sortFunction = 'mean'
    addAt = 'back'
    addFunction = 'mean'
    applyMode = 'normal'
    applyParameter = '1'
    applyFunction = 'mean'
    groupFunction = 'sum'
    sortOrder = 'desc'
    outputPrecision = None

    focusedFrames = list(range(len(inputFrames)))

    for (optionName, optionValue) in input['args'].ordered_args:
        multiFrameActions = ['print_precision', 'select_mode', 'filter_mode', 'sort_function', 'sort_order', 'add_at', 'add_function', 'apply_mode', 'apply_function', 'group_function', 'apply_parameter', 'join', 'file', 'pickle', 'split_column', 'split_icolumn', 'split_row', 'split_irow', 'focus_frames', 'defocus_frames']
        if optionName not in multiFrameActions:
            for _index, (frameOptions, frame) in enumerate(inputFrames):
                if (_index) not in focusedFrames:
                    continue
                if optionName == 'transpose':
                    frame = DataframeActions.transpose(frame)
                elif optionName == 'index_column':
                    columnIds = DataframeActions.getColumnIds(frame, optionValue, selectMode)
                    if len(columnIds) > 1 and not args.quiet:
                        print('WARNING: cannot set multiple index columns', file=sys.stderr)
                    if (len(columnIds) > 0):
                        frame = DataframeActions.setIndexColumnByIdx(frame, columnIds[0])
                elif optionName == 'index_icolumn':
                    columnIds = DataframeActions.sliceToColumnIds(frame, optionValue)
                    if len(columnIds) > 1 and not args.quiet:
                        print('WARNING: cannot set multiple index columns', file=sys.stderr)
                    if (len(columnIds) > 0):
                        frame = DataframeActions.setIndexColumnByIdx(frame, columnIds[0])
                elif optionName == 'reset_index':
                    frame = DataframeActions.resetIndex(frame)
                elif optionName == 'ignore_columns':
                    columnIds = DataframeActions.getColumnIds(frame, optionValue, selectMode, True)
                    frame = DataframeActions.dropColumnsByIds(frame, columnIds)
                elif optionName == 'ignore_icolumns':
                    columnIds = DataframeActions.sliceToColumnIds(frame, optionValue)
                    frame = DataframeActions.dropColumnsByIds(frame, columnIds)
                elif optionName == 'ignore_rows':
                    rowIds = DataframeActions.getRowIds(frame, optionValue, selectMode, True)
                    frame = DataframeActions.dropRowsByIds(frame, rowIds)
                elif optionName == 'ignore_irows':
                    rowIds = DataframeActions.sliceToRowIds(frame, optionValue)
                    frame = DataframeActions.dropRowsByIds(frame, rowIds)
                elif optionName == 'select_columns':
                    columnIds = DataframeActions.getColumnIds(frame, optionValue, selectMode, True)
                    frame = DataframeActions.selectColumnsByIds(frame, columnIds)
                elif optionName == 'select_icolumns':
                    columnIds = DataframeActions.sliceToColumnIds(frame, optionValue)
                    frame = DataframeActions.selectColumnsByIds(frame, columnIds)
                elif optionName == 'select_rows':
                    rowIds = DataframeActions.getRowIds(frame, optionValue, selectMode, True)
                    frame = DataframeActions.selectRowsByIds(frame, rowIds)
                elif optionName == 'select_irows':
                    rowIds = DataframeActions.sliceToRowIds(frame, optionValue)
                    frame = DataframeActions.selectRowsByIds(frame, rowIds)
                elif optionName == 'filter_column':
                    columnIds = DataframeActions.getColumnIds(frame, optionValue[0], selectMode, True)
                    if len(columnIds) == 0:
                        if not args.quiet:
                            print(f"WARNING: column '{optionValue[0]}' not found", file=sys.stderr)
                    elif len(optionValue) < 2:
                        if not args.quiet:
                            print('WARNING: nothing provided to filter after', file=sys.stderr)
                    else:
                        frame = DataframeActions.filterRowsByColumnData(frame, columnIds, optionValue[1:], filterMode)
                elif optionName == 'filter_icolumns':
                    if not isSliceType(optionValue[0]):
                        if not args.quiet:
                            print('WARNING: invalid slice provided to filter data', file=sys.stderr)
                    elif len(optionValue) < 2:
                        if not args.quiet:
                            print('WARNING: nothing provided to filter after', file=sys.stderr)
                    else:
                        columnIds = DataframeActions.sliceToColumnIds(frame, SliceType()(optionValue[0]))
                        frame = DataframeActions.filterRowsByColumnData(frame, columnIds, optionValue[1:], filterMode)
                elif optionName == 'filter_row':
                    rowIds = DataframeActions.getRowIds(frame, optionValue[0], selectMode, True)
                    if len(rowIds) == 0:
                        if not args.quiet:
                            print(f"WARNING: row '{optionValue[0]}' not found", file=sys.stderr)
                    elif len(optionValue) < 2:
                        if not args.quiet:
                            print('WARNING: nothing provided to filter after', file=sys.stderr)
                    else:
                        frame = DataframeActions.filterColumnsByRowData(frame, rowIds, optionValue[1:], filterMode)
                elif optionName == 'filter_irows':
                    if not isSliceType(optionValue[0]):
                        if not args.quiet:
                            print('WARNING: invalid slice provided to filter data', file=sys.stderr)
                    elif len(optionValue) < 2:
                        if not args.quiet:
                            print('WARNING: nothing provided to filter after', file=sys.stderr)
                    else:
                        rowIds = DataframeActions.sliceToRowIds(frame, SliceType()(optionValue[0]))
                        frame = DataframeActions.filterColumnsByRowData(frame, rowIds, optionValue[1:], filterMode)
                elif optionName == 'reverse_columns':
                    frame = DataframeActions.reverseColumns(frame)
                elif optionName == 'reverse_rows':
                    frame = DataframeActions.reverseRows(frame)
                elif optionName == 'sort_columns':
                    frame = DataframeActions.sortColumns(frame, sortFunction, sortOrder)
                elif optionName == 'sort_rows':
                    frame = DataframeActions.sortRows(frame, sortFunction, sortOrder)
                elif optionName == 'sort_by_icolumns':
                    if 'index' in optionValue:
                        if (len(optionValue) > 1 and not args.quiet):
                            print('WARNING: cannot combine sorting after index with other columns', file=sys.stderr)
                        frame = DataframeActions.sortRowsByColumnIds(frame, 'index', groupFunction)
                    else:
                        columnIds = DataframeActions.sliceToColumnIds(frame, optionValue)
                        frame = DataframeActions.sortRowsByColumnIds(frame, columnIds, sortFunction, sortOrder)
                elif optionName == 'sort_by_irows':
                    if 'columns' in optionValue:
                        if (len(optionValue) > 1 and not args.quiet):
                            print('WARNING: cannot combine sorting after columns with other rows', file=sys.stderr)
                        frame = DataframeActions.sortColumnsByRowIds(frame, 'columns', groupFunction)
                    else:
                        rowIds = DataframeActions.sliceToRowIds(frame, optionValue)
                        frame = DataframeActions.sortColumnsByRowIds(frame, rowIds, sortFunction, sortOrder)
                elif optionName == 'sort_by_columns':
                    columnIds = DataframeActions.getColumnIds(frame, optionValue, selectMode)
                    frame = DataframeActions.sortRowsByColumnIds(frame, columnIds, sortFunction, sortOrder)
                elif optionName == 'sort_by_rows':
                    rowIds = DataframeActions.getRowIds(frame, optionValue, selectMode)
                    frame = DataframeActions.sortColumnsByRowIds(frame, rowIds, sortFunction, sortOrder)
                elif optionName == 'normalise_to':
                    frame = DataframeActions.normaliseToConstant(frame, optionValue)
                elif optionName == 'normalise_to_column':
                    applyColumnIds = DataframeActions.getColumnIds(frame, optionValue, selectMode)
                    if len(applyColumnIds) > 1 and not args.quiet:
                        print('WARNING:', file=sys.stderr)
                    targetColumnIds = DataframeActions.sliceToColumnIds(frame, slice(None))
                    if len(applyColumnIds) > 0 and len(targetColumnIds) > 0:
                        frame = DataframeActions.applyOnColumns(frame, applyColumnIds[0], targetColumnIds, 'div')
                elif optionName == 'normalise_to_icolumn':
                    applyColumnIds = DataframeActions.sliceToColumnIds(frame, optionValue)
                    if (len(applyColumnIds) > 1 and not args.quiet):
                        print('WARNING: can only normalise to a single column', file=sys.stderr)
                    targetColumnIds = DataframeActions.sliceToColumnIds(frame, slice(None))
                    if len(applyColumnIds) > 0 and len(targetColumnIds) > 0:
                        frame = DataframeActions.applyOnColumns(frame, applyColumnIds[0], targetColumnIds, 'div')
                elif optionName == 'normalise_to_row':
                    applyRowIds = DataframeActions.getRowIds(frame, optionValue, selectMode)
                    if len(applyRowIds) > 1 and not args.quiet:
                        print('WARNING:', file=sys.stderr)
                    targetRowIds = DataframeActions.sliceToRowIds(frame, slice(None))
                    if len(applyRowIds) > 0 and len(targetRowIds) > 0:
                        frame = DataframeActions.applyOnRows(frame, applyRowIds[0], targetRowIds, 'div')
                elif optionName == 'normalise_to_irow':
                    applyRowIds = DataframeActions.sliceToRowIds(frame, optionValue)
                    if (len(applyRowIds) > 1 and not args.quiet):
                        print('WARNING: can only normalise to a single row', file=sys.stderr)
                    targetRowIds = DataframeActions.sliceToRowIds(frame, slice(None))
                    if len(applyRowIds) > 0 and len(targetRowIds) > 0:
                        frame = DataframeActions.applyOnRows(frame, applyRowIds[0], targetRowIds, 'div')
                elif optionName == 'abs':
                    frame = DataframeActions.abs(frame)
                elif optionName == 'apply_irows':
                    applyRowIds = DataframeActions.sliceToRowIds(frame, optionValue[0])
                    targetRowIds = DataframeActions.sliceToRowIds(frame, optionValue[1:]) if len(optionValue) > 1 else []
                    frame = DataframeActions.applyOnRows(frame, applyRowIds, targetRowIds, applyFunction, applyParameter, applyMode)
                elif optionName == 'apply_rows':
                    applyRowIds = DataframeActions.getRowIds(frame, optionValue[0])
                    targetRowIds = DataframeActions.getRowIds(frame, optionValue[1:]) if len(optionValue) > 1 else []
                    frame = DataframeActions.applyOnRows(frame, applyRowIds, targetRowIds, applyFunction, applyParameter, applyMode)
                elif optionName == 'apply_icolumns':
                    applyColumnIds = DataframeActions.sliceToColumnIds(frame, optionValue[0])
                    targetColumnIds = DataframeActions.sliceToColumnIds(frame, optionValue[1:]) if len(optionValue) > 1 else []
                    frame = DataframeActions.applyOnColumns(frame, applyColumnIds, targetColumnIds, applyFunction, applyParameter, applyMode)
                elif optionName == 'apply_columns':
                    applyColumnIds = DataframeActions.getColumnIds(frame, optionValue[0])
                    targetColumnIds = DataframeActions.getColumnIds(frame, optionValue[1:]) if len(optionValue) > 1 else []
                    frame = DataframeActions.applyOnColumns(frame, applyColumnIds, targetColumnIds, applyFunction, applyParameter, applyMode)
                elif optionName == 'group_by_irows':
                    if 'columns' in optionValue:
                        if (len(optionValue) > 1 and not args.quiet):
                            print('WARNING: cannot combine grouping after columns with other rows', file=sys.stderr)
                        frame = DataframeActions.groupByRowIds(frame, 'columns', groupFunction)
                    else:
                        rowIds = DataframeActions.sliceToRowIds(frame, optionValue)
                        frame = DataframeActions.groupByRowIds(frame, rowIds, groupFunction)
                elif optionName == 'group_by_icolumns':
                    if 'index' in optionValue:
                        if (len(optionValue) > 1 and not args.quiet):
                            print('WARNING: cannot combine grouping after index with other columns', file=sys.stderr)
                        frame = DataframeActions.groupByColumnIds(frame, 'index', groupFunction)
                    else:
                        columnIds = DataframeActions.sliceToColumnIds(frame, optionValue)
                        frame = DataframeActions.groupByColumnIds(frame, columnIds, groupFunction)
                elif optionName == 'group_by_rows':
                    rowIds = DataframeActions.getRowIds(frame, optionValue, selectMode)
                    frame = DataframeActions.groupByRowIds(frame, rowIds, groupFunction)
                elif optionName == 'group_by_columns':
                    columnIds = DataframeActions.getColumnIds(frame, optionValue, selectMode)
                    frame = DataframeActions.groupByColumnIds(frame, columnIds, groupFunction)
                elif optionName == 'add_column':
                    frame = DataframeActions.addColumn(frame, optionValue, addFunction, addAt)
                elif optionName == 'add_row':
                    frame = DataframeActions.addRow(frame, optionValue, addFunction, addAt)
                elif optionName == 'data_offset':
                    frame = DataframeActions.addConstant(frame, optionValue)
                elif optionName == 'data_scale':
                    frame = DataframeActions.scaleConstant(frame, optionValue)
                elif optionName == 'column_names':
                    frame = DataframeActions.renameColumns(frame, optionValue)
                elif optionName == 'row_names':
                    frame = DataframeActions.renameRows(frame, optionValue)
                elif optionName == 'drop_nan':
                    frame = DataframeActions.dropNaN(frame)
                elif optionName == 'drop_any_nan':
                    frame = DataframeActions.dropNaN(frame, True)
                elif optionName == 'print':
                    DataframeActions.printFrames(frameOptions.filenames, frame, _index, len(inputFrames), outputPrecision)
                    doneSomething = True

                inputFrames[_index] = (frameOptions, frame)
        else:
            if optionName == 'focus_frames':
                focusedFrames = sorted(getSliceTypeIds(range(len(inputFrames)), optionValue))
            elif optionName == 'defocus_frames':
                for f in getSliceTypeIds(range(len(inputFrames)), optionValue):
                    if f in focusedFrames:
                        focusedFrames.remove(f)
            elif optionName == 'print_precision':
                outputPrecision = None if optionValue == 'default' else int(optionValue)
            elif optionName == 'select_mode':
                selectMode = optionValue
            elif optionName == 'filter_mode':
                filterMode = optionValue
            elif optionName == 'sort_function':
                sortFunction = optionValue
            elif optionName == 'sort_order':
                sortOrder = optionValue
            elif optionName == 'add_at':
                addAt = optionValue
            elif optionName == 'add_function':
                addFunction = optionValue
            elif optionName == 'apply_mode':
                applyMode = optionValue
            elif optionName == 'apply_function':
                applyFunction = optionValue
            elif optionName == 'group_function':
                groupFunction = optionValue
            elif optionName == 'apply_parameter':
                applyParameter = optionValue
            elif optionName == 'join':
                newOptions = copy.deepcopy(inputOptions)
                newOptions.filenames = []
                newOptions.frameCount = 1
                frontDefocusedFrames = [inputFrames[x] for x in range(focusedFrames[0]) if x not in focusedFrames]
                backDefocusedFrames = [inputFrames[x] for x in range(focusedFrames[0], len(inputFrames)) if x not in focusedFrames]
                joinedFrame = DataframeActions.joinFrames([frame for (_, frame) in [inputFrames[x] for x in focusedFrames]], optionValue)
                inputFrames = frontDefocusedFrames + [(newOptions, joinedFrame)] + backDefocusedFrames
                focusedFrames = [len(frontDefocusedFrames)]
            elif optionName == 'file':
                DataframeActions.framesToCSV([frame for (_, frame) in [inputFrames[x] for x in focusedFrames]], optionValue, inputOptions.delimiter, args.quiet, outputPrecision)
                doneSomething = True
            elif optionName == 'pickle':
                DataframeActions.framesToPickle([frame for (_, frame) in [inputFrames[x] for x in focusedFrames]], optionValue, args.quiet)
                doneSomething = True
            elif optionName in ['split_column', 'split_icolumn', 'split_row', 'split_irow']:
                newInputFrames = []
                newFocusedFrames = []
                for _index, (frameOptions, frame) in enumerate(inputFrames):
                    if (_index) not in focusedFrames:
                        newInputFrames.append((frameOptions, frame))
                        continue
                    newFrames = []
                    if optionName == 'split_icolumn':
                        newFrames = DataframeActions.splitFramesByColumnIdx([frame], optionValue)
                    elif optionName == 'split_column':
                        columnIds = DataframeActions.getColumnIds(frame, optionValue, selectMode)
                        newFrames = DataframeActions.splitFramesByColumnIdx([frame], columnIds[0])
                    elif optionName == 'split_irow':
                        newFrames = DataframeActions.splitFramesByRowIdx([frame], optionValue)
                    elif optionName == 'split_row':
                        rowIds = DataframeActions.getRowIds(frame, optionValue, selectMode)
                        newFrames = DataframeActions.splitFramesByRowIdx([frame], rowIds[0])
                    newFocusedFrames.extend(range(len(newInputFrames), len(newInputFrames) + len(newFrames)))
                    for newFrame in newFrames:
                        newOptions = copy.deepcopy(inputOptions)
                        newOptions.filenames = []
                        newOptions.frameCount = 1
                        newInputFrames.append((newOptions, newFrame))
                focusedFrames = newFocusedFrames
                inputFrames = newInputFrames

        if args.debug:
            print(f"DEBUG: applied {optionName}", file=sys.stderr)
        gc.collect()

    defocusedFrames = [i for i in range(len(inputFrames)) if i not in focusedFrames]
    if not args.quiet and len(defocusedFrames) > 0:
        print(f'WARNING: {len(defocusedFrames)} frames are defocused and will be ignored', file=sys.stderr)

    inputOptions.traceCount = 0
    inputOptions.frameCount = 0
    for _index, (options, frame) in enumerate(inputFrames):
        if _index not in focusedFrames:
            continue
        inputOptions.frameCount += 1
        inputOptions.traceCount += len([x for x in frame.columns if str(x) not in inputOptions.traceSpecialColumns and str(x) not in inputOptions.frameSpecialColumns])
        frame = frame.replace([numpy.inf, -numpy.inf], numpy.nan)
        frame = frame.where(pandas.notnull(frame), None)
        inputFrames[_index] = (options, frame)

    if inputOptions.frameCount == 0:
        if not args.quiet:
            print(f'WARNING: files {", ".join(inputFileNames)} did turn into any valid dataframes', file=sys.stderr)
        continue

    inputOptions.inputIndex = totalInputCount
    totalTraceCount += inputOptions.traceCount
    totalFrameCount += inputOptions.frameCount
    totalInputCount += 1

    updateRange(subplotGrid, [inputOptions.col + (inputOptions.colspan - 1), inputOptions.row + (inputOptions.rowspan - 1)])
    if (inputOptions.row not in subplotGridDefinition):
        subplotGridDefinition[inputOptions.row] = {}
    if (inputOptions.col not in subplotGridDefinition[inputOptions.row]):
        subplotGridDefinition[inputOptions.row][inputOptions.col] = copy.deepcopy({
            'rowspan': inputOptions.rowspan,
            'colspan': inputOptions.colspan,
            'secondary_y': inputOptions.y_secondary,
            'title': inputOptions.title,
            'traces': 0,
            'frames': 0,
            'colours': args.colours,
            'palette': args.palette,
            'palette_count': args.palette_count,
            'palette_local': inputOptions.colour_cycle == 'subplot',
            'palette_reverse': args.palette_reverse,
            'palette_index': args.palette_start,
            'palette_opacity': args.palette_opacity,
            'palette_cmap': args.palette_cmap
        })

    subplotGridDefinition[inputOptions.row][inputOptions.col]['rowspan'] = max(inputOptions.rowspan, subplotGridDefinition[inputOptions.row][inputOptions.col]['rowspan'])
    subplotGridDefinition[inputOptions.row][inputOptions.col]['colspan'] = max(inputOptions.colspan, subplotGridDefinition[inputOptions.row][inputOptions.col]['colspan'])
    subplotGridDefinition[inputOptions.row][inputOptions.col]['secondary_y'] = inputOptions.y_secondary or subplotGridDefinition[inputOptions.row][inputOptions.col]['secondary_y']
    if inputOptions.title is not None:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['title'] = inputOptions.title

    if len(inputOptions.subplot_colours) > 0:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['colours'] = copy.copy(inputOptions.subplot_colours)
    if inputOptions.subplot_palette is not None:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['palette'] = inputOptions.subplot_palette
    if inputOptions.subplot_palette_reverse is not None:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['palette_reverse'] = inputOptions.subplot_palette_reverse
    if inputOptions.subplot_palette_count is not None:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['palette_count'] = inputOptions.subplot_palette_count
    if inputOptions.subplot_palette_start is not None:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['palette_index'] = inputOptions.subplot_palette_start
    if inputOptions.subplot_palette_opacity is not None:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['palette_opacity'] = inputOptions.subplot_palette_opacity
    if inputOptions.subplot_palette_opacity is not None:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['palette_opacity'] = inputOptions.subplot_palette_opacity
    if inputOptions.subplot_palette_cmap is not None:
        subplotGridDefinition[inputOptions.row][inputOptions.col]['palette_cmap'] = inputOptions.subplot_palette_cmap

    inputOptions.subplotTraceIndex = subplotGridDefinition[inputOptions.row][inputOptions.col]['traces']
    subplotGridDefinition[inputOptions.row][inputOptions.col]['traces'] += inputOptions.traceCount
    subplotGridDefinition[inputOptions.row][inputOptions.col]['frames'] += inputOptions.frameCount

    data.append({'options': copy.deepcopy(inputOptions), 'frames': [f.where(pandas.notnull(f), None) for f in [f for i, (_, f) in enumerate(inputFrames) if i in focusedFrames]]})

    gc.collect()


scriptPath = None
secondaryScriptPaths = []
# Separate python outputs from actual output put the first script
# into scriptPath and all others into secondaryScriptPaths,
# scriptPath is master all others are secondary
for x in args.output:
    if x.lower().endswith('.py'):
        if scriptPath is None:
            scriptPath = x
        else:
            secondaryScriptPaths.append(x)

# Remove scripts from output
args.output = [x for x in args.output if not x.lower().endswith('.py')]


if doneSomething and not args.browser and len(args.output) == 0 and scriptPath is None:
    exit(0)
elif len(args.output) == 0 and scriptPath is None:
    args.browser = True

if totalTraceCount == 0:
    if not args.quiet:
        print('No input data available for plotting.')
    exit(0)

# Plotting script will be executed in this path context
if scriptPath is not None:
    scriptContext = os.path.abspath(os.path.dirname(scriptPath))
else:
    scriptContext = os.path.abspath(os.getcwd())

# Converting paths to new relative paths to the plotting script
# In case a script is saved, those paths are still valid no matter from where its called
for i, p in enumerate(args.output):
    if not os.path.isabs(p):
        args.output[i] = os.path.relpath(p, scriptContext)


if args.theme == 'palette' and args.colour_debug:
    print('Colour Palettes:')

subplotGrid = [{k: int(v) for (k, v) in dim.items()} for dim in subplotGrid]

for r in range(1, int(subplotGrid[1]['max']) + 1):
    for c in range(1, int(subplotGrid[0]['max']) + 1):
        if r in subplotGridDefinition and c in subplotGridDefinition[r]:
            subplot = subplotGridDefinition[r][c]
            if subplot['palette_count'] is None:
                if subplot['palette_local']:
                    subplot['palette_count'] = subplot['traces'] if args.per_trace_colours else subplot['frames'] if args.per_frame_colours else 1
                else:
                    subplot['palette_count'] = totalTraceCount if args.per_trace_colours else totalFrameCount if args.per_frame_colours else totalInputCount
                subplot['palette_count'] = max(0, subplot['palette_count'] - len(subplot['colours']))
            subplot['colours'].extend([f"rgba({int(255*r)}, {int(255*g)}, {int(255*b)}, {subplot['palette_opacity']})" for (r, g, b) in seaborn.color_palette(subplot['palette'], subplot['palette_count'])])

            subplot['cmap'] = None

            if subplot['palette_reverse']:
                subplot['colours'].reverse()

            if args.theme == 'palette' and args.colour_debug:
                print(f'    subplot @ [{r}, {c}, local({subplot["palette_local"]})]: ' + ' '.join(subplot['colours']))
globalPaletteIndex = args.palette_start

legendEntries = []

plotFd = None
if (scriptPath is None):
    args.script_only = False
    plotFd, plotScriptName = tempfile.mkstemp()
    plotScript = open(plotScriptName, 'w+')
else:
    plotScriptName = os.path.abspath(scriptPath)
    plotScript = open(plotScriptName, 'w+')

plotScript.write(f"""#!/usr/bin/env python3
#
# Generated by {__prog__} {__version__}, available at {__url__}

import os
import sys
import shutil
import subprocess
import tempfile
import argparse
import plotly
import plotly.graph_objects as go
import plotly.io as pio
from plotly.subplots import make_subplots

# Disable MathJAX to avoid unnecessary message in pdf output
pio.kaleido.scope.mathjax = None

parser = argparse.ArgumentParser(description="plots the contained figure")
parser.add_argument("--font-size", help="font size (default %(default)s)", type=int, default={args.font_size})
parser.add_argument("--font-colour", help="font colour (default %(default)s)", default='{args.font_colour}')
parser.add_argument("--font-family", help="font family (default %(default)s)", default='{args.font_family}')
parser.add_argument("--width", help="width of output file (default %(default)s)", type=int, default={args.width})
parser.add_argument("--height", help="height of output (default %(default)s)", type=int, default={args.height})
parser.add_argument("--output", "-o", help="output file (html, png, jpeg, pdf...) (default %(default)s)", type=str, nargs="+", default={args.output})
parser.add_argument("--browser", help="open plot in browser", action="store_true")
parser.add_argument("--quiet", "-q", help="no warnings and don't open output file", action="store_true")

args = parser.parse_args()

if len(args.output) == 0:
    args.browser = True
""")


subplotTitles = []

plotScript.write(f"""\n\nplotly.io.templates.default = '{"plotly_white" if args.theme == "palette" else args.theme}'

fig = make_subplots(
    cols={subplotGrid[0]['max']},
    rows={subplotGrid[1]['max']},
    shared_xaxes={args.x_share},
    shared_yaxes={args.y_share},
    y_title={args.y_master_title},
    x_title={args.x_master_title},
    vertical_spacing={args.vertical_spacing},
    horizontal_spacing={args.horizontal_spacing},
    specs=[""")
for r in range(1, subplotGrid[1]['max'] + 1):
    plotScript.write("\n        [")
    for c in range(1, subplotGrid[0]['max'] + 1):
        if (r in subplotGridDefinition and c in subplotGridDefinition[r]):
            plotScript.write(f"{{'rowspan': {subplotGridDefinition[r][c]['rowspan']}, 'colspan': {subplotGridDefinition[r][c]['colspan']}, 'secondary_y': {subplotGridDefinition[r][c]['secondary_y']}}}, ")
            subplotTitles.append('' if subplotGridDefinition[r][c]['title'] is None else subplotGridDefinition[r][c]['title'])
        else:
            plotScript.write("None,")
    plotScript.write("],")
plotScript.write(f"""
    ],
    subplot_titles={subplotTitles}
)""")

currentInputIndex = None
frameIndex = 0
traceIndex = 0
inputIndex = 0

for input in data:
    options = input['options']
    frames = input['frames']
    subplot = subplotGridDefinition[options.row][options.col]
    plotRange = []
    inputTraceIndex = 0
    inputFrameIndex = 0
    multiCategory = False

    if options.traceCount == 0:
        print(f"WARNING: frame {inputFrameIndex} from input files {', '.join(options.filenames)} has no traces")
        continue

    for frame in frames:
        # NaN cannot be plotted or used, cast it to None
        # Drop only columns/rows NaN values and replace NaN with None
        frame = frame.dropna(how='all', axis=0)
        frame = frame.replace({numpy.nan: None})
        frame.index = frame.index.to_series().replace({numpy.nan: None})
        frameTraceIndex = 0

        expandDistribution = None
        if options.distribution_mode == 'aggregated':
            if options.plot in ['violin', 'box']:
                if not all(frame.index.map(isFloat)):
                    if not args.quiet:
                        print(f"WARNING: cannot expand distribution for non numeric index in frame {inputFrameIndex}", file=sys.stderr)
                else:
                    expandDistribution = frame.index.map(float).to_list()
            else:
                if not args.quiet:
                    print(f"WARNING: distribution mode aggregated has no effect on plot type '{options.plot}' in frame {inputFrameIndex}", file=sys.stderr)

        _categories = None
        for specialFrameColumn in options.frameSpecialColumns:
            if specialFrameColumn not in frame.columns:
                continue
            for colIndex in range(len(frame.columns)):
                colName = str(frame.columns[colIndex])
                if (colName == options.special_column_prefix + 'category') and _categories is None:
                    _categories = ['' if x is None else x for x in frame.iloc[:, colIndex].values.tolist()]

        if options.plot not in ['heatmap']:
            for colIndex, _ in enumerate(frame.columns):
                col = str(frame.columns[colIndex])
                if col in options.traceSpecialColumns or col in options.frameSpecialColumns:
                    continue

                if options.trace_colours and frameTraceIndex < len(options.trace_colours):
                    fillcolour = options.trace_colours[frameTraceIndex]
                else:
                    colourIndex = (subplot['palette_index'] if subplot['palette_local'] else globalPaletteIndex) % len(subplot['colours'])
                    fillcolour = subplot['colours'][colourIndex]
                markercolour = options.line_colour

                _errors_symmetric = True
                _errors_pos = None
                _errors_neg = None
                _bases = None
                _labels = None
                _colours = None
                for nextColIndex in range(colIndex + 1, colIndex + 1 + len(options.traceSpecialColumns) if colIndex + 1 + len(options.traceSpecialColumns) <= len(frame.columns) else len(frame.columns)):
                    nextCol = str(frame.columns[nextColIndex])
                    if (nextCol not in options.traceSpecialColumns):
                        break
                    if (nextCol == options.special_column_prefix + 'error') and (_errors_pos is None):
                        _errors_pos = [x if (x is not None) else 0 for x in frame.iloc[:, nextColIndex].values.tolist()]
                    elif (nextCol == options.special_column_prefix + 'error+') and (_errors_pos is None):
                        _errors_symmetric = False
                        _errors_pos = [x if (x is not None) else 0 for x in frame.iloc[:, nextColIndex].values.tolist()]
                    elif (nextCol == options.special_column_prefix + 'error-') and (_errors_neg is None):
                        _errors_symmetric = False
                        _errors_neg = [x if (x is not None) else 0 for x in frame.iloc[:, nextColIndex].values.tolist()]
                    elif (nextCol == options.special_column_prefix + 'offset') and (_bases is None):
                        _bases = [x if (x is not None) else 0 for x in frame.iloc[:, nextColIndex].values.tolist()]
                    elif (nextCol == options.special_column_prefix + 'label') and (_labels is None):
                        _labels = frame.iloc[:, nextColIndex].values.tolist()
                    elif (nextCol == options.special_column_prefix + 'colour') and (_colours is None) and (frameTraceIndex >= len(options.trace_colours)):
                        _colours = frame.iloc[:, nextColIndex].values.tolist()
                        _colours = [c if c is not None else fillcolour for c in _colours]

                if (options.plot == 'line'):
                    ydata = frame.iloc[:, colIndex].values.tolist() if not options.vertical else list(frame.index)
                    xdata = frame.iloc[:, colIndex].values.tolist() if options.vertical else list(frame.index)
                    updateRange(plotRange, [xdata, ydata])
                elif (options.plot == 'bar'):
                    ydata = frame.iloc[:, colIndex].tolist() if options.vertical else list(frame.index)
                    xdata = frame.iloc[:, colIndex].tolist() if not options.vertical else list(frame.index)
                    if _bases is not None:
                        rxdata = xdata
                        rydata = ydata
                        if (options.horizontal):
                            rxdata = [a + b if (a is not None and b is not None) else a if a is not None else b for a, b in zip(xdata, _bases)]
                        else:
                            rydata = [a + b if (a is not None and b is not None) else a if a is not None else b for a, b in zip(ydata, _bases)]
                        updateRange(plotRange, [rxdata, rydata])
                    else:
                        updateRange(plotRange, [xdata, ydata])
                    if _categories is not None:
                        multiCategory = True
                        if options.horizontal:
                            ydata = [_categories, ydata]
                        else:
                            xdata = [_categories, xdata]
                else:  # Box and Violin
                    rawData = frame.iloc[:, colIndex].values
                    data = [x for x in rawData if x is not None]
                    if expandDistribution is not None:
                        localExpand = [x for (x, y) in zip(expandDistribution, rawData) if y is not None]
                        # Expansion is using ceiling to avoid dropping of data points. Side effect is that it might lift the distribution a little bit up
                        data = list(numpy.repeat(localExpand, numpy.ceil(data).astype(numpy.int64)))
                    index = None
                    ydata = index if not options.vertical else data
                    xdata = index if options.vertical else data
                    updateRange(plotRange, [xdata, ydata])

                if options.offsetgroups == 'auto':
                    offsetgroup = options.subplotTraceIndex + inputFrameIndex + frameTraceIndex + 1 if args.bar_mode == 'group' else None
                else:
                    if inputTraceIndex < len(options.offsetgroups):
                        offsetgroup = options.offsetgroups[inputTraceIndex]
                    else:
                        offsetgroup = options.offsetgroups[-1]

                traceName = col

                if (inputTraceIndex < len(options.trace_names)):
                    traceName = options.trace_names[inputTraceIndex]
                elif (options.use_name is not None):
                    traceName = options.use_name

                showInLegend = options.legend_entries == 'all'
                if traceName not in legendEntries:
                    if options.legend_entries == 'unique':
                        showInLegend = True
                    legendEntries.append(traceName)

                if options.plot == 'line':
                    lineMarker = options.line_markers[-1] if len(options.line_markers) <= inputTraceIndex else options.line_markers[inputTraceIndex]
                    plotScript.write(f"""
fig.add_trace(go.Scatter(
    name='{traceName}',
    legendgroup='{traceName}',
    showlegend={showInLegend},
    mode='{options.line_mode}',""")
                    if (_colours is not None):
                        plotScript.write(f"""
    marker_color={_colours},""")
                    else:
                        plotScript.write(f"""
{commentColour}    marker_color='{fillcolour}',""")
                    plotScript.write(f"""
{commentColour}    line_color='{fillcolour}',
{commentColour}    fillcolor='{fillcolour}', # Currently not supported through script, using default
    stackgroup='{'stackgroup-' + str(inputIndex) if options.line_stack else ''}',
    marker_symbol='{lineMarker}',
    marker_size={options.line_marker_size},
    fill='{options.line_fill}',
    line_dash='{options.line_dash}',
    line_shape='{options.line_shape}',
    line_width={options.line_width},
    y={ydata},
    x={xdata},""")
                    if (_labels is not None):
                        plotScript.write(f"""
    text={_labels},
    textposition='{options.line_text_position}',""")
                    if (_errors_pos is not None or _errors_neg is not None):
                        plotScript.write(f"""
    error_{'y' if options.horizontal else 'x'}=dict(
        visible={options.show_error},
        type='data',
        symmetric={_errors_symmetric},
        array={_errors_pos},
        arrayminus={_errors_neg},
    ),""")
                    plotScript.write(f"""
    opacity={options.opacity},
), col={options.col}, row={options.row}, secondary_y={options.y_secondary})
""")
                elif options.plot == 'bar':
                    plotScript.write(f"""
fig.add_trace(go.Bar(
    name='{traceName}',
    legendgroup='{traceName}',
    showlegend={showInLegend},
    orientation='{'v' if options.vertical else 'h'}',""")
                    if (_colours is not None):
                        plotScript.write(f"""
    marker_color={_colours},""")
                    else:
                        plotScript.write(f"""
{commentColour}    marker_color='{fillcolour}',""")
                    plotScript.write(f"""
{commentColour}    marker_line_color='{markercolour}',
    marker_line_width={options.line_width},
    width={options.bar_width},
    offset={options.bar_shift},
    offsetgroup={offsetgroup},
    y={ydata},
    x={xdata},""")
                    if (_labels is not None):
                        plotScript.write(f"""
    text={_labels},""")
                    plotScript.write(f"""
    texttemplate='{options.bar_text_template}',
    textposition='{options.bar_text_position}',""")
                    if (_bases is not None):
                        plotScript.write(f"""
    base={_bases},""")
                    if (_errors_pos is not None or _errors_neg is not None):
                        plotScript.write(f"""
    error_{'x' if options.horizontal else 'y'}=dict(
        visible={options.show_error},
        type='data',
        symmetric={_errors_symmetric},
        array={_errors_pos},
        arrayminus={_errors_neg},
    ),""")
                    plotScript.write(f"""
    opacity={options.opacity},
), col={options.col}, row={options.row}, secondary_y={options.y_secondary})
""")
                elif options.plot == 'box':
                    markercolour = options.line_colour
                    plotScript.write(f"""
fig.add_trace(go.Box(
    name='{traceName}',
    legendgroup='{traceName}',
    showlegend={showInLegend},
    y={ydata},
    x={xdata},
    boxpoints=False,
    boxmean={True if options.box_mean == 'line' else False},
    width={options.box_width},
{commentColour}    fillcolor='{fillcolour}',
{commentColour}    line_color='{markercolour}',
{commentColour}    marker_color='{markercolour}',
    line_width={options.line_width},
    orientation='{'v' if options.vertical else 'h'}',
    opacity={options.opacity},
), col={options.col}, row={options.row}, secondary_y={options.y_secondary})
""")
                    if options.box_mean == 'dot':
                        plotScript.write(f"""
fig.add_trace(go.Scatter(
    name='mean_{traceName}',
    legendgroup='{traceName}',
    showlegend=False,
    x={xdata if options.vertical else [numpy.mean([float(x) for x in xdata if isFloat(x)])]},
    y={ydata if not options.vertical else [numpy.mean([float(y) for y in ydata if isFloat(y)])]},
{commentColour}    fillcolor='{fillcolour}',
{commentColour}    line_color='{markercolour}',
    line_width={options.line_width},
    opacity={options.opacity},
), col={options.col}, row={options.row}, secondary_y={options.y_secondary})
""")
                elif options.plot == 'violin':
                    if options.violin_mode == 'halfhalf':
                        side = 'negative' if inputTraceIndex % 2 == 0 else 'positive'
                    elif options.violin_mode.endswith('half') or options.violin_mode.endswith('halfgroup'):
                        side = 'negative' if options.violin_mode.startswith('neg') else 'positive'
                    else:
                        side = 'both'
                    markercolour = options.line_colour
                    plotScript.write(f"""
fig.add_trace(go.Violin(
    name='{traceName}',
    legendgroup='{traceName}',
    showlegend={showInLegend},
    scalegroup='trace{inputTraceIndex}',
    y={ydata},
    x={xdata},
{commentColour}    fillcolor='{fillcolour}',
{commentColour}    line_color='{options.line_colour}',
{commentColour}    marker_color='{markercolour}',
    line_width={options.line_width},
    side='{side}',
    width={options.violin_width},
    scalemode='width',
    points={False if options.violin_points == 'none' else options.violin_points},
    jitter={options.violin_jitter},
    orientation='{'v' if options.vertical else 'h'}',
    meanline_visible={True if options.violin_mean == 'line' else False},
    box_visible={True if options.violin_mean == 'box' else False},
    opacity={options.opacity},
), col={options.col}, row={options.row}, secondary_y={options.y_secondary})
""")

                traceIndex += 1
                frameTraceIndex += 1
                inputTraceIndex += 1
                if subplot['palette_local']:
                    subplot['palette_index'] += 1 if args.per_trace_colours else 0
                else:
                    globalPaletteIndex += 1 if args.per_trace_colours else 0
            inputFrameIndex += 1
            frameIndex += 1
            if subplot['palette_local']:
                subplot['palette_index'] += 1 if args.per_frame_colours else 0
            else:
                globalPaletteIndex += 1 if args.per_frame_colours else 0
        inputIndex += 1
        if subplot['palette_local']:
            subplot['palette_index'] += 1 if args.per_input_colours else 0
        else:
            globalPaletteIndex += 1 if args.per_input_colours else 0
    else:
        # Generate continuous colour scale as we will need it here
        if subplot['cmap'] is None:
            try:
                seaborn_cmap = seaborn.color_palette(subplot['palette'], as_cmap=True)
                if subplot['palette_cmap'] in ['log', 'rlog']:
                    cmap_range = numpy.logspace(numpy.log10(0.1), numpy.log10(1.1), 255) - 0.1
                    if subplot['palette_cmap'] == 'rlog':
                        cmap_range = numpy.flip(1 - cmap_range)
                else:
                    cmap_range = numpy.arange(0.0, 1.0 + (1.0 / 255), 1.0 / 255)
                if subplot['palette_reverse']:
                    cmap_range = numpy.flip(cmap_range)

                subplot['cmap'] = [f'rgb({r}, {g}, {b})' for (r, g, b) in [map(numpy.uint8, 255 * numpy.array(seaborn_cmap(i)[:3])) for i in cmap_range]]
            except Exception:
                raise 'ERROR: could not generated continuous colour scale, is the chosen seaborn palette compatible?'

        data = frame.values.T.tolist() if options.vertical else frame.values.tolist()
        yaxis = list(frame.columns) if options.vertical else list(frame.index)
        xaxis = list(frame.columns) if not options.vertical else list(frame.index)
        updateRange(plotRange, [xaxis, yaxis])
        if options.plot == 'heatmap':
            plotScript.write(f"""
fig.add_trace(go.Heatmap(
    y={yaxis},
    x={xaxis},
    z={data},
{commentColour}    colorscale={subplot['cmap']},
    showscale={options.colour_scale == 'show'},
), col={options.col}, row={options.row}, secondary_y={options.y_secondary})
            """)
            pass

    # Find out if we need left, right and bottom margin:
    if defaultLeftMargin is None and options.col == 1 and options.y_title and not options.y_secondary:
        defaultLeftMargin = True
    if defaultTopMargin is None and options.row == 1 and options.title:
        # default top margin is 100 which is a bit much for just having subplot titles:
        defaultTopMargin = True
        if args.margin_t is None and args.margins is None:
            args.margin_t = 40
    if defaultRightMargin is None and options.col + options.colspan - 1 == subplotGrid[0]['max'] and options.y_title is not None and options.y_secondary:
        defaultRightMargin = True
    if defaultBottomMargin is None and options.row + options.rowspan - 1 == subplotGrid[1]['max'] and options.x_title is not None:
        defaultBottomMargin = True

    # If line width was not explicitly set, set the axis line width for the multi category axis to one
    if multiCategory and options.horizontal and not options.y_line_width_forced:
        options.y_line_width = 1
    if multiCategory and options.vertical and not options.x_line_width_forced:
        options.x_line_width = 1

    plotScript.write("\n\n")
    plotScript.write("# Subplot specific options:\n")
    plotScript.write(f"fig.update_yaxes(type='{options.y_type}', rangemode='{options.y_range_mode}', automargin={True if options.y_title_standoff is None else False}, title_standoff={options.y_title_standoff}, col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"fig.update_xaxes(type='{options.x_type}', rangemode='{options.x_range_mode}', automargin={True if options.x_title_standoff is None else False}, title_standoff={options.x_title_standoff}, col={options.col}, row={options.row})\n")
    plotScript.write(f"fig.update_yaxes(showline={options.y_line_width > 0}, linewidth={options.y_line_width}, linecolor={options.y_colour}, gridcolor={options.y_grid_colour}, col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"fig.update_xaxes(showline={options.x_line_width > 0}, linewidth={options.x_line_width}, linecolor={options.x_colour}, gridcolor={options.x_grid_colour}, col={options.col}, row={options.row})\n")
    plotScript.write(f"fig.update_coloraxes(showscale={options.colour_scale == 'show'}, col={options.col}, row={options.row})\n")
    plotScript.write("# Multi-category options:\n")
    plotScript.write(f"fig.update_yaxes(showdividers={options.y_line_width > 0}, dividercolor={options.y_colour}, dividerwidth={options.y_line_width}, col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"fig.update_xaxes(showdividers={options.x_line_width > 0}, dividercolor={options.x_colour}, dividerwidth={options.x_line_width}, col={options.col}, row={options.row})\n")
    plotScript.write(f"{'# ' if not options.y_hide else ''}fig.update_yaxes(visible=False, showticklabels=False, showgrid=True, zeroline=False, row={options.row}, col={options.col}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"{'# ' if not options.x_hide else ''}fig.update_xaxes(visible=False, showticklabels=False, showgrid=True, zeroline=False, row={options.row}, col={options.col})\n")
    plotScript.write(f"{'# ' if options.y_title is None else ''}fig.update_yaxes(title_text='{options.y_title}', col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"{'# ' if options.x_title is None else ''}fig.update_xaxes(title_text='{options.x_title}', col={options.col}, row={options.row})\n")
    plotScript.write(f"fig.update_yaxes(tickcolor={options.y_colour}, tickformat='{options.y_tick_format}', ticksuffix='{options.y_tick_suffix}', tickprefix='{options.y_tick_prefix}', col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"fig.update_xaxes(tickcolor={options.x_colour}, tickformat='{options.x_tick_format}', ticksuffix='{options.x_tick_suffix}', tickprefix='{options.x_tick_prefix}', col={options.col}, row={options.row})\n")
    if options.y_range_from is not None or options.y_range_to is not None:
        options.y_range_from = options.y_range_from if options.y_range_from is not None else plotRange[1]['min']
        options.y_range_to = options.y_range_to if options.y_range_to is not None else plotRange[1]['max']
        plotScript.write(f"fig.update_yaxes(range=[{options.y_range_from}, {options.y_range_to}], col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    if options.x_range_from is not None or options.x_range_to is not None:
        options.x_range_from = options.x_range_from if options.x_range_from is not None else plotRange[0]['min']
        options.x_range_to = options.x_range_to if options.x_range_to is not None else plotRange[0]['max']
        plotScript.write(f"fig.update_xaxes(range=[{options.x_range_from}, {options.x_range_to}], col={options.col}, row={options.row})\n")
    plotScript.write(f"# fig.update_yaxes(range=[{plotRange[0]['min']}, {plotRange[0]['max']}], col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"# fig.update_xaxes(range=[{plotRange[1]['min']}, {plotRange[1]['max']}], col={options.col}, row={options.row})\n")
    plotScript.write(f"fig.update_yaxes(tickmode='{options.y_tickmode}', ticks='{options.y_ticks}', nticks={options.y_nticks}, tick0='{options.y_tick0}', dtick='{options.y_dtick}', tickvals={options.y_tickvals}, ticktext={options.y_ticktext}, tickangle={options.y_tickangle}, col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"fig.update_xaxes(tickmode='{options.x_tickmode}', ticks='{options.x_ticks}', nticks={options.x_nticks}, tick0='{options.x_tick0}', dtick='{options.x_dtick}', tickvals={options.x_tickvals}, ticktext={options.x_ticktext}, tickangle={options.x_tickangle}, col={options.col}, row={options.row})\n")
    plotScript.write(f"{'# ' if not options.y_tick_hide else ''}fig.update_yaxes(showticklabels=False, col={options.col}, row={options.row}, secondary_y={options.y_secondary})\n")
    plotScript.write(f"{'# ' if not options.x_tick_hide else ''}fig.update_xaxes(showticklabels=False, col={options.col}, row={options.row})\n")
    plotScript.write("\n")

if violinMode.endswith('halfgroup'):
    violinMode = 'group'
elif violinMode.endswith('half'):
    violinMode = 'overlay'

plotScript.write('# Global modes and paramters:\n')
plotScript.write(f"fig.update_layout(title={args.master_title})\n")
plotScript.write(f"fig.update_layout(barmode='{args.bar_mode}', boxmode='{args.box_mode}', violinmode='{violinMode}')\n")
plotScript.write(f"fig.update_layout(bargap={args.bar_gap}, bargroupgap={args.bar_group_gap}, boxgap={args.box_gap}, boxgroupgap={args.box_group_gap}, violingap={args.violin_gap}, violingroupgap={args.violin_group_gap})\n")

plotScript.write("\n# Layout Legend\n")
plotScript.write(f"fig.update_layout(showlegend={args.legend_show}, legend_traceorder='{args.legend_order}', legend_tracegroupgap={args.legend_groupgap})\n")
plotScript.write(f"{'# ' if args.legend_y_anchor is None else ''}fig.update_layout(legend_yanchor='{'auto' if args.legend_y_anchor is None else args.legend_y_anchor}')\n")
plotScript.write(f"{'# ' if args.legend_x_anchor is None else ''}fig.update_layout(legend_xanchor='{'auto' if args.legend_x_anchor is None else args.legend_x_anchor}')\n")
plotScript.write(f"fig.update_layout(legend=dict(x={args.legend_x}, y={args.legend_y}, orientation='{'v' if args.legend_vertical else 'h'}', bgcolor='rgba(255,255,255,0)'))\n")

plotScript.write("\n# Layout Plot and Background\n")
plotScript.write(f"{commentBackgroundColour}fig.update_layout(paper_bgcolor='{args.background_colour}', plot_bgcolor='{args.background_colour}')\n")

args.margin_b = args.margin_b if args.margin_b is not None else args.margins if args.margins is not None else None if defaultBottomMargin else 0
args.margin_l = args.margin_l if args.margin_l is not None else args.margins if args.margins is not None else None if defaultLeftMargin else 0
args.margin_r = args.margin_r if args.margin_r is not None else args.margins if args.margins is not None else None if defaultRightMargin else 0
args.margin_t = args.margin_t if args.margin_t is not None else args.margins if args.margins is not None else None if defaultTopMargin else 0
args.margin_pad = args.margin_pad if args.margin_pad is not None else args.margins if args.margins is not None else None if defaultPadMargin else 0

plotScript.write(f"fig.update_layout(margin=dict(t={args.margin_t}, l={args.margin_l}, r={args.margin_r}, b={args.margin_b}, pad={args.margin_pad}))\n")

plotScript.write("\n# Plot Font\n")
plotScript.write(f"""fig.update_layout(font=dict(
    family=args.font_family,
    size=args.font_size,
{commentColour}    color=args.font_colour
))
""")

plotScript.write("""
# Execute addon file if found
filename, fileext = os.path.splitext(__file__)
if (os.path.exists(f'{filename}_addon{fileext}')):
    exec(open(f'{filename}_addon{fileext}').read())

""")

plotScript.write("""

if args.browser:
    fig.show()
if len(args.output) > 0:
    for output in args.output:
        outputFormat = output.lower().split('.')[-1]
        if outputFormat == 'html':
            fig.write_html(output)
        else:
            fig.write_image(output, width=args.width, height=args.height)

        if not args.quiet:
            print(f'Saved to {output}')
            try:
                if sys.platform == "win32":
                    os.startfile(output)
                else:
                    opener = "open" if sys.platform == "darwin" else "xdg-open"
                    subprocess.call([opener, output], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
            except Exception:
                print(f'Could not open {output}!')
""")

plotScript.close()
if args.browser or len(args.output) > 0:
    cmdLine = ['python', plotScriptName]
    if args.browser:
        cmdLine.append('--browser')
    if args.quiet:
        cmdLine.append('--quiet')
    subprocess.check_call(cmdLine, cwd=scriptContext)

if scriptPath is None:
    os.close(plotFd)
    os.remove(plotScriptName)
else:
    for s in secondaryScriptPaths:
        shutil.copy(scriptPath, s)
    if not args.quiet:
        for s in [scriptPath] + secondaryScriptPaths:
            print(f"Script saved to {s}")
