# Source code for spacepy.plot.spectrogram

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-

    """
    Create and plot generic 'spectrograms' for space science.
    This is not a signal processing routine and does not apply
    Fourier transforms (or similar) to the data. The functionality
    provided here is the binning (and averaging) of multi-dimensional
    to provide a 2D output map of some quantity as a function of two
    parameters. An example would be particle data from a satellite mission:
    electron flux, at a given energy, can be binned as a function of
    both time and McIlwain L, then plotted as a 2D color-map,
    colloquially known as a spectrogram.

    In many other settings 'spectrogram' refers to a transform of data
    from the time domain to the frequency domain, and the subsequent plotting
    of some quantity (e.g., power spectral density) as a function of time and
    frequency. To approximate this functionality for, e.g., time-series magnetic field
    data you would first calculate a the power spectral density and then use
    :class:`spectrogram` to rebin the data for visualization.

    Authors: Brian Larsen and Steve Morley
    Institution: Los Alamos National Laboratory
    Contact: balarsen@lanl.gov, smorley@lanl.gov
    Los Alamos National Laboratory

    Copyright 2011 Los Alamos National Security, LLC.

    .. autosummary::
        :template: clean_class.rst
        :toctree:

        spectrogram
    """

    import bisect
    import copy
    import datetime

    import numpy as np
    import matplotlib
    import matplotlib.axes
    import matplotlib.collections as mcoll
    from matplotlib.dates import date2num, num2date
    from matplotlib.colors import LogNorm
    import matplotlib.transforms as mtransforms
    import matplotlib.pyplot as plt

    import spacepy.datamodel as dm
    import spacepy.toolbox as tb
    from  . import utils as spu

    __contact__ = 'Brian Larsen, balarsen@lanl.gov'

    __all__ = ['spectrogram', 'simpleSpectrogram']

    class spectrogram(dm.SpaceData):
        """
        This class rebins data to produce a 2D data map that can be plotted as a spectrogram

        It is meant to be used on arbitrary data series.  The first series "x" is
        plotted on the abscissa and second series "y" is plotted on the ordinate and
        the third series "z" is plotted in color.

        The series are not passed in independently but instead inside a
        :class:`~spacepy.datamodel.SpaceData` container.

        Parameters
        ==========
        data : :class:`~spacepy.datamodel.SpaceData`
            The data for the spectrogram, the variables to be used default to
            "Epoch" for x, "Energy" for y, and "Flux" for z.  Other names are
            specified using the 'variables' keyword.  All keywords override .attrs
            contents.

        Other Parameters
        ================
        variables : list
            keyword containing the names of the variables to use for the spectrogram
            the list is a list of the SpaceData keys in x, y, z, order
        bins : list
            if the name "bins" is not specified in the .attrs of the dmarray variable
            this specifies the bins for each variable in a
            [[xbins], [ybins]] format
        xlim : list
            if the name "lim" is not specified in the .attrs of the dmarray variable
            this specifies the limit for the x variable [xlow, xhigh]
        ylim : list
            if the name "lim" is not specified in the .attrs of the dmarray variable
            this specifies the limit for the y variable [ylow, yhigh]
        zlim : list
            if the name "lim" is not specified in the .attrs of the dmarray variable
            this specifies the limit for the z variable [zlow, zhigh]
        extended_out : bool (optional)
            if this is True add more information to the output data model (default True)

        Notes
        =====
        Helper routines are planned to
        facilitate the creation of the SpaceData container if the data are not in the format.

        Examples
        --------
        >>> import spacepy.datamodel as dm
        >>> import numpy as np
        >>> import spacepy.plot as splot
        >>> sd = dm.SpaceData()
        >>> sd['radius'] = dm.dmarray(2*np.sin(np.linspace(0,30,500))+4, attrs={'units':'km'})
        >>> sd['day_of_year'] = dm.dmarray(np.linspace(74,77,500))
        >>> sd['1D_dataset'] = dm.dmarray(np.random.normal(10,3,500)*sd['radius'])
        >>> spec = splot.spectrogram(sd, variables=['day_of_year', 'radius', '1D_dataset'])
        >>> ax = spec.plot()

        .. autosummary::

            ~spectrogram.plot

        .. automethod:: plot

        """

    #    TODO
    #    ====
    #    Allow for the input of a list of SpaceData objects for different sats
    #    Make "subclasses" that allow for data to be passed in directly avoiding the data model

        def __init__(self, data, **kwargs):
            """
            """
            super(spectrogram, self).__init__()
            ## setup a default dictionary to step through to set values from kwargs
            self.specSettings = dm.SpaceData()
            self.specSettings['variables'] = ['Epoch', 'Energy', 'Flux']
            self.specSettings['bins'] = None  # this is the linspace over the range with sqrt() of the len bins
            self.specSettings['xlim'] = None
            self.specSettings['ylim'] = None
            self.specSettings['zlim'] = None
            self.specSettings['extended_out'] = True
            self.specSettings['axisDates'] = False

            # if the key exists in kwargs replace setting with it, otherwise its an error
            for key in kwargs:
                if key not in self.specSettings:
                    raise(KeyError('Invalid keyword specified ' + str(key)))
                self.specSettings[key] = kwargs[key]

            # check to see if the variables are in the spacedata
            for var in self.specSettings['variables']:
                if not var in data:  # TODO could check other capitalization
                    raise(KeyError('"{0}" not found in the input data'.format(var) ))

            # if the variables are empty error and quit
            if len(data[self.specSettings['variables'][0]]) == 0:
                raise(ValueError('No {0} data passed in'.format(self.specSettings['variables'][0])))
            if len(data[self.specSettings['variables'][1]]) == 0:
                raise(ValueError('No {0} data passed in'.format(self.specSettings['variables'][1])))
            if len(data[self.specSettings['variables'][2]]) == 0:
                raise(ValueError('No {0} data passed in'.format(self.specSettings['variables'][2])))

            # set limits, keywords override those in the data
            #if (self.specSettings['xlim'] is None) and (self.specSettings['bins'] is None):
            if self.specSettings['xlim'] is None:
                try:
                    if 'lim' in data[self.specSettings['variables'][0]].attrs:
                        self.specSettings['xlim'] = (data[self.specSettings['variables'][0]].attrs['lim'][0],
                                                    data[self.specSettings['variables'][0]].attrs['lim'][1])
                    else:
                        dum1 = np.min(data[self.specSettings['variables'][0]]).tolist() #TODO: tolist here is a workaround for a bug in
                        dum2 = np.max(data[self.specSettings['variables'][0]]).tolist() #      datamodel's min method
                        self.specSettings['xlim'] = (dum1, dum2)
                except AttributeError: # was a numpy array not dmarray
                    self.specSettings['xlim'] = (np.min(data[self.specSettings['variables'][0]]),
                                                np.max(data[self.specSettings['variables'][0]]))
            if self.specSettings['ylim'] is None:
                try:
                    if 'lim' in data[self.specSettings['variables'][1]].attrs:
                        self.specSettings['ylim'] = (data[self.specSettings['variables'][1]].attrs['lim'][0],
                                                    data[self.specSettings['variables'][1]].attrs['lim'][1])
                    else:
                        self.specSettings['ylim'] = (np.min(data[self.specSettings['variables'][1]]),
                                                    np.max(data[self.specSettings['variables'][1]]))
                except AttributeError: # was a numpy array not dmarray
                    self.specSettings['ylim'] = (np.min(data[self.specSettings['variables'][1]]),
                                                np.max(data[self.specSettings['variables'][1]]))
            if self.specSettings['zlim'] is None:
                try:
                    if 'lim' in data[self.specSettings['variables'][2]].attrs:
                        self.specSettings['zlim'] = (data[self.specSettings['variables'][2]].attrs['lim'][0],
                                                    data[self.specSettings['variables'][2]].attrs['lim'][1])
                    else:
                        self.specSettings['zlim'] = (np.min(data[self.specSettings['variables'][2]]),
                                                    np.max(data[self.specSettings['variables'][2]]))
                except AttributeError: # was a numpy array not dmarray
                    self.specSettings['zlim'] = (np.min(data[self.specSettings['variables'][2]]),
                                                np.max(data[self.specSettings['variables'][2]]))

            # are the axes dates?
            forcedate = [False] * 2
            if isinstance(data[self.specSettings['variables'][0]][0], datetime.datetime):
                forcedate[0] = True
            if isinstance(data[self.specSettings['variables'][1]][0], datetime.datetime):
                forcedate[1] = True
            self.specSettings['axisDates'] = forcedate

            # set default bins
            if self.specSettings['bins'] is None:
                # since it is not set by keyword was it set in the datamodel?
                attr_bins = ['bins' in data[var].attrs for var in self.specSettings['variables']]
                if dm.dmarray(attr_bins[0:2]).all():
                    self.specSettings['bins'] = [dm.dmarray(data[self.specSettings['variables'][0]].attrs['bins']),
                                                 dm.dmarray(data[self.specSettings['variables'][1]].attrs['bins']),]
                    # TODO this is not a hard extension to doing one with bins and one default
                else:
                    # use the toolbox version of linspace so it works on dates
                    self.specSettings['bins'] = [dm.dmarray(tb.linspace(self.specSettings['xlim'][0],
                                                     self.specSettings['xlim'][1],
                                                     np.sqrt(len(data[self.specSettings['variables'][0]])))),
                        dm.dmarray(tb.linspace(self.specSettings['ylim'][0],
                                                     self.specSettings['ylim'][1],
                                                     np.sqrt(len(data[self.specSettings['variables'][1]]))))]

            # copy all the used keys
            for key in self.specSettings['variables']:
                self[key] = data[key]
                try:
                    self[key].attrs = data[key].attrs
                except:
                    pass
            # do the spectrogram
            self._computeSpec()

        def _computeSpec(self):
            """
            Method operates on the input data to bin up the spectrogram and adds it
            to the spectrogram class data
            """
            # this is here for in the future when we take a list a SpaceData objects
            sz = (self.specSettings['bins'][1].shape[0]-1, self.specSettings['bins'][0].shape[0]-1)
            overall_sum = dm.dmarray(np.zeros(sz, dtype=np.double))
            overall_count = dm.dmarray(np.zeros(sz, dtype=np.long))

            # the valid range for the histograms
            _range = [self.specSettings['xlim'], self.specSettings['ylim']]

            # if x/y is a time need to convert it to numbers (checking first element)
            var_time = [False]*2
            for ivar, var in enumerate(self.specSettings['variables']):
                if isinstance(self[var][0], datetime.datetime):
                    try:
                        self.specSettings['bins'][ivar] = matplotlib.dates.date2num(self.specSettings['bins'][ivar])
                    except AttributeError: # it is already changed to date2num
                        pass
                    try: #weird issue with arrays and date2num
                        _range[ivar] = matplotlib.dates.date2num(_range[ivar].tolist())
                    except AttributeError:
                        try: # ugg if this is not an array this breaks
                            _range[ivar] = [matplotlib.dates.date2num(val.tolist()) for val in _range[ivar]]
                        except AttributeError:
                            _range[ivar] = [matplotlib.dates.date2num(val) for val in _range[ivar]]
                    var_time[ivar] = True

            # ok not as a dmarray since it is local now
            plt_data = np.vstack((self[self.specSettings['variables'][0]], self[self.specSettings['variables'][1]]))

            for ival, val in enumerate(var_time):
                if val:
                    plt_data[ival] = date2num(plt_data[ival])

            if plt_data.dtype.name == 'object':  # why was this an abject
                plt_data = plt_data.astype(float)

            # go through and get rid of "bad" counts
            zdat = np.ma.masked_outside(self[self.specSettings['variables'][2]],
                                        self.specSettings['zlim'][0],
                                        self.specSettings['zlim'][1])
            zind = ~zdat.mask
            # ma has the annoying feature of if all the masks are the same just giving one value
            try:
                if len(zind) == len(zdat):
                    pass
            except TypeError: # no len to a scalar
                # ok not as a dmarray since it is local now
                zind = np.asarray([zind]*len(zdat))

            # get the number in each bin
            H, xedges, yedges = np.histogram2d(plt_data[0, zind],
                                    plt_data[1, zind],
                                    bins = self.specSettings['bins'],
                                    range = _range,
                                    )
            # this is here for in the future when we take a list a SpaceData objects
            np.add(overall_count,
                   np.require(H.transpose(), dtype=overall_count.dtype),
                   overall_count)

            # get the sum in each bin
            H, xedges, yedges = np.histogram2d(plt_data[0, zind],
                                    plt_data[1, zind],
                                    bins = self.specSettings['bins'],
                                    range = _range,
                                    weights = zdat.data[zind]
                                    )
            np.add(overall_sum, H.transpose(), overall_sum)

            overall_count = np.ma.masked_array(overall_count, overall_count == 0)
            data = np.ma.divide(overall_sum, overall_count)

            ## for plotting
            #ind0 = data.data <= 0
            #data[ind0] = np.ma.masked # add to the mask
            #data = np.ma.log10(data)
            self['spectrogram'] = dm.SpaceData()
            self['spectrogram'].attrs = self.specSettings
            self['spectrogram']['spectrogram'] = dm.dmarray(data,
                attrs={'name':str(self.specSettings['variables'][0]) + ' ' + str(self.specSettings['variables'][1]),
                       'xedges':'xedges',
                       'yedges':'yedges',})
            self['spectrogram']['xedges'] = dm.dmarray(xedges,
                attrs={'name':str(self.specSettings['variables'][0]),
                       'lim':[self.specSettings['xlim']],})
            self['spectrogram']['yedges'] = dm.dmarray(yedges,
                attrs={'name':str(self.specSettings['variables'][1]),
                       'lim':[self.specSettings['ylim']],})
            if self.specSettings['extended_out']:
                self['spectrogram']['count'] = overall_count
                self['spectrogram']['sum'] = overall_sum

        def add_data(self, data):
            """
            Add another SpaceData with same keys, etc. to spectrogram instance

            Examples
            --------
            >>> import spacepy.datamodel as dm
            >>> import numpy as np
            >>> import spacepy.plot as splot
            >>> sd = dm.SpaceData()
            >>> sd['radius'] = dm.dmarray(2*np.sin(np.linspace(0,30,500))+4, attrs={'units':'km'})
            >>> sd['day_of_year'] = dm.dmarray(np.linspace(74,77,500))
            >>> sd['1D_dataset'] = dm.dmarray(np.random.normal(10,3,500)*sd['radius'])
            >>> sd2 = dm.dmcopy(sd)
            >>> sd2['radius'] = dm.dmarray(2*np.cos(np.linspace(0,30,500))+4, attrs={'units':'km'})
            >>> sd2['1D_dataset'] = dm.dmarray(np.random.normal(10,3,500)*sd2['radius'])
            >>> spec = splot.spectrogram(sd, variables=['day_of_year', 'radius', '1D_dataset'])
            >>> spec.add_data(sd2)
            >>> ax = spec.plot()
            """
            if not self.specSettings['extended_out']:
                raise(NotImplementedError('Cannot add data to a spectrogram unless "extended_out" was True on initial creation'))
            b = spectrogram(data, **self.specSettings)
            # if they are both masked keep them that way
            mask = self['spectrogram']['count'].mask & b['spectrogram']['count'].mask
            # turn off the mask by setting sum and count to zero where it masked for self an be sure b mask doesn't it self
            self['spectrogram']['count'][self['spectrogram']['count'].mask] = 0
            b['spectrogram']['count'][b['spectrogram']['count'].mask] = 0
            # put the mask back they are both bad
            self['spectrogram']['count'].mask = mask
            b['spectrogram']['count'].mask = mask
            self['spectrogram']['count'] += b['spectrogram']['count']
            self['spectrogram']['sum'] += b['spectrogram']['sum']
            self['spectrogram']['spectrogram'][...] = np.ma.divide(self['spectrogram']['sum'], self['spectrogram']['count'])

        def plot(self, target=None, loc=111, figsize=None, **kwargs):
            """
            Plot the spectrogram

            Other Parameters
            ================
            title : str
                plot title (default '')
            xlabel : str
                x axis label (default '')
            ylabel : str
                y axis label (default '')
            colorbar_label : str
                colorbar label (default '')
            DateFormatter : matplotlib.dates.DateFormatter
                The formatting to use on the dates on the x-axis (default matplotlib.dates.DateFormatter("%d %b %Y"))
            zlog : bool
                plot the z variable on a log scale (default True)
            cmap : matplotlib Colormap
                colormap instance to use
            colorbar : bool
                plot the colorbar (default True)
            axis : matplotlib axis object
                axis to plot the spectrogram to
            zlim : np.array
                array like 2 element that overrides (interior) the spectrogram zlim (default spectrogram.specSettings['zlim'])
            figsize : tuple (optional)
                tuple of size to pass to figure(), None does the default
            """
            # go through the passed in kwargs to plot and look at defaults
            import matplotlib.pyplot as plt
            plotSettings_keys = ('title', 'xlabel', 'ylabel', 'DateFormatter',
                                 'zlim', 'colorbar', 'colorbar_label', 'zlog',
                                 'xlim', 'ylim', 'figsize', 'cmap')
            for key in kwargs:
                if key not in plotSettings_keys:
                    raise(KeyError('Invalid keyword argument to plot(), "' + key + '"'))

            self.plotSettings = dm.SpaceData()
            for key in ['title', 'xlabel', 'ylabel']:
                if key in kwargs:
                    self.plotSettings[key] = kwargs[key]
                else:
                    self.plotSettings[key] = ''
            if 'zlog' in kwargs:
                self.plotSettings['zlog'] = kwargs['zlog']
            else:
                self.plotSettings['zlog'] = True

            if 'DateFormatter' in kwargs:
                self.plotSettings['DateFormatter'] = kwargs['DateFormatter']
            else:
                self.plotSettings['DateFormatter'] = matplotlib.dates.DateFormatter("%d %b %Y")
            if 'zlim' in kwargs:
                self.plotSettings['zlim'] = kwargs['zlim']
            elif 'zlim' in self.specSettings:
                self.plotSettings['zlim'] = self.specSettings['zlim']
            else:
                self.plotSettings['zlim'] = self.specSettings['zlim']
            if 'colorbar' in kwargs:
                self.plotSettings['colorbar'] =  kwargs['colorbar']
            else:
                self.plotSettings['colorbar'] = True
            if 'colorbar_label' in kwargs:
                self.plotSettings['colorbar_label'] = kwargs['colorbar_label']
            else:
                self.plotSettings['colorbar_label'] = ''
            if 'xlim' in kwargs:
                self.plotSettings['xlim'] = kwargs['xlim']
            elif 'xlim' in self.specSettings:
                self.plotSettings['xlim'] = self.specSettings['xlim']
            else:
                self.plotSettings['xlim'] = None
            if 'ylim' in kwargs:
                self.plotSettings['ylim'] = kwargs['ylim']
            elif 'ylim' in self.specSettings:
                self.plotSettings['ylim'] = self.specSettings['ylim']
            else:
                self.plotSettings['ylim'] = None

            fig, ax = spu.set_target(target, loc=loc, figsize=figsize)

            bb = np.ma.masked_outside(self['spectrogram']['spectrogram'], *self.plotSettings['zlim'])
            if 'cmap' in kwargs:
                self.plotSettings['cmap'] = kwargs['cmap']
            else:
                self.plotSettings['cmap'] = matplotlib.cm.rainbow

            if self.plotSettings['zlog']:
                pcm = ax.pcolormesh(self['spectrogram']['xedges'], self['spectrogram']['yedges'], np.asarray(bb),
                                    norm=LogNorm(), cmap=self.plotSettings['cmap'], vmin=self.plotSettings['zlim'][0],
                                    vmax=self.plotSettings['zlim'][1])
            else:
                pcm = ax.pcolormesh(self['spectrogram']['xedges'], self['spectrogram']['yedges'], np.asarray(bb),
                                    cmap=self.plotSettings['cmap'], vmin=self.plotSettings['zlim'][0],
                                    vmax=self.plotSettings['zlim'][1])

            if self.specSettings['axisDates'][0]:
                time_ticks = self._set_ticks_to_time(ax, 'x')
            elif self.specSettings['axisDates'][1]:
                time_ticks = self._set_ticks_to_time(ax, 'y')
            ax.set_title(self.plotSettings['title'])
            ax.set_xlabel(self.plotSettings['xlabel'])
            ax.set_ylabel(self.plotSettings['ylabel'])
            if self.plotSettings['ylim'] != None:
                ax.set_ylim(self.plotSettings['ylim'])
            if self.plotSettings['xlim'] != None:
                ax.set_xlim(self.plotSettings['xlim'])

            if self.plotSettings['colorbar']:
                cb =plt.colorbar(pcm)
                cb.set_label(self.plotSettings['colorbar_label'])
            return ax

        def vslice(self, value):
            """
            slice a spectrogram at a given position along the x axis, maintains
            variable names from spectrogram

            Parameters
            ==========
            value : float or datetime.datetime
                the value to slice the spectrogram at

            Returns
            =======
            out : datamodel.SpaceData
                spacedata containing the slice
            """
            # using bisect find the index of the spectrogram to use
            if isinstance(value, datetime.datetime):
                value = date2num(value)
            ind = bisect.bisect_right(self['spectrogram']['xedges'], value)
            ans = dm.SpaceData()
            ans[self['spectrogram'].attrs['variables'][1]] = tb.bin_edges_to_center(self['spectrogram']['yedges'])
            ans['yedges'] = self['spectrogram']['yedges'].copy()
            ans['xedges'] = self['spectrogram']['xedges'][ind:ind+2].copy()
            ans[self['spectrogram'].attrs['variables'][2]] = self['spectrogram']['spectrogram'][:,ind:ind+1]
            return ans

        def hslice(self, value):
            """
            slice a spectrogram at a given position along the y axis, maintains
            variable names from spectrogram

            Parameters
            ==========
            value : float or datetime.datetime
                the value to slice the spectrogram at

            Returns
            =======
            out : datamodel.SpaceData
                spacedata containing the slice
            """
            # using bisect find the index of the spectrogram to use
            if isinstance(value, datetime.datetime):
                value = date2num(value)
            ind = bisect.bisect_right(self['spectrogram']['yedges'], value)
            ans = dm.SpaceData()
            ans[self['spectrogram'].attrs['variables'][0]] = tb.bin_edges_to_center(self['spectrogram']['xedges'])
            ans['yedges'] = self['spectrogram']['yedges'][ind:ind+2].copy()
            ans['xedges'] = self['spectrogram']['xedges'].copy()
            ans[self['spectrogram'].attrs['variables'][2]] = self['spectrogram']['spectrogram'][ind, :]
            return ans

        def _set_ticks_to_time(self, axis, xy):
            """
            given the axis change the ticks to times
            """
            timeFmt = self.plotSettings['DateFormatter']
            if xy == 'x':
                ticks = axis.get_xticks()
                axis.set_xticklabels(matplotlib.dates.num2date(ticks))
                axis.xaxis.set_major_formatter(timeFmt)
                axis.get_figure().autofmt_xdate()
            elif xy == 'y':
                ticks = axis.get_yticks()
                axis.set_yticklabels(matplotlib.dates.num2date(ticks))
                axis.yaxis.set_major_formatter(timeFmt)
                axis.get_figure().autofmt_ydate()

        def __str__(self):
            return "<spectrogram object>"

        __repr__ = __str__

    def simpleSpectrogram(*args, **kwargs):
        """
        Plot a spectrogram given Z or X,Y,Z. This is a wrapper around pcolormesh()
        that can handle Y being a 2d array of time dependent bins. Like in the
        Van Allen Probes HOPE and MagEIS data files.

        Parameters
        ==========
        *args : 1 or 3 arraylike
            Call Signatures::

            simpleSpectrogram(Z, **kwargs)
            simpleSpectrogram(X, Y, Z, **kwargs)

        Other Parameters
        ================
        zlog : bool
            Plot the color with a log colorbar (default: True)
        ylog : bool
            Plot the Y axis with a log scale (default: True)
        alpha : scalar (0-1)
            The alpha blending value (default: None)
        cmap : string
            The name of the colormap to use (default: system default)
        vmin : float
            Minimum color value (default: Z.min(), if log non-zero min)
        vmax : float
            Maximum color value (default: Z.max())
        ax : matplotlib.axes
            Axes to plot the spectrogram on (default: None - new axes)
        cb : bool
            Plot a colorbar (default: True)
        cbtitle : string
            Label to go on the colorbar (default: None)

        Returns
        =======
        ax : matplotlib.axes._subplots.AxesSubplot
            Matplotlib axes object that the plot is on
        """
        if len(args) not in [1,3]:
            raise(TypeError("simpleSpectrogram, takes Z or X, Y, Z"))

        if len(args) == 1: # just Z in, makeup an X and Y
            Z = np.ma.masked_invalid(np.asarray(args[0])) # make sure not a dmarray or VarCopy
            X = np.arange(Z.shape[0]+1)
            Y = np.arange(Z.shape[1]+1)
        else:
            # we really want X and Y to be one larger than Z
            X = np.asarray(args[0]) # make sure not a dmarray or VarCopy
            Y = np.asarray(args[1]) # make sure not a dmarray or VarCopy
            Z = np.ma.masked_invalid(np.asarray(args[2]))
            if X.shape[0] == Z.shape[0]: # same length, expand X
                X = tb.bin_center_to_edges(X) # hopefully evenly spaced
            if len(Y.shape) == 1: # 1d, just use as axis
                Y = tb.bin_center_to_edges(Y) # hopefully evenly spaced
            elif len(Y.shape) == 2:
                Y_orig = Y
                # 2d this is time dependent and thus need to overplot several
                Y = tb.unique_columns(Y, axis=1)
                if Y.shape[1] == Z.shape[1]: # hopefully evenly spaced
                    Y_uniq = Y
                    Y_tmp = np.empty((Y.shape[0], Y.shape[1]+1), dtype=Y.dtype)
                    for ii in range(Y.shape[0]):
                        Y_tmp[ii] = tb.bin_center_to_edges(Y[ii])
                    Y = Y_tmp

        # deal with all the default keywords
        zlog    = kwargs.pop('zlog', True)
        ylog    = kwargs.pop('ylog', True)
        alpha   = kwargs.pop('alpha', None)
        cmap    = kwargs.pop('cmap', None)
        vmin    = kwargs.pop('vmin', np.min(Z) if not zlog else np.min(Z[np.nonzero(Z)]))
        vmax    = kwargs.pop('vmax', np.max(Z))
        ax      = kwargs.pop('ax', None)
        cb      = kwargs.pop('cb', True)
        cbtitle = kwargs.pop('cbtitle', None)

        # the first case is that X, Y are 1d and Z is 2d, just make the plot
        if ax is None:
            fig = plt.figure()
            ax = fig.add_subplot(111)
        else:
            fig = ax.get_figure()
        Z = Z.filled(0)
        if len(Y.shape) > 1:
            for yy in Y_uniq:
                # which indices in X have these values
                ind = (yy == Y_orig).all(axis=1)
                print(Y_orig[ind][0].min(), Y_orig[ind][0].max())
                Y_tmp = np.zeros_like(Y_orig)
                Y_tmp[ind] = Y_orig[ind][0]
                Z_tmp = np.zeros_like(Z)
                Z_tmp[ind] = Z[ind]
                pc = ax.pcolormesh(X, Y_tmp[ind][0], Z_tmp.T,
                                   norm=LogNorm(vmin=vmin, vmax=vmax), cmap=cmap,
                                   alpha=alpha)
        else:
            pc = ax.pcolormesh(X, Y, Z.T, norm=LogNorm(vmin=vmin, vmax=vmax),
                               cmap=cmap,
                               alpha=alpha)
        if ylog: ax.set_yscale('log')

        if cb: # add a colorbar
            cb_ = fig.colorbar(pc)
            cb_.set_label(cbtitle)
        return ax