Example #1
0
    def set_units(self, *args, **kwargs):
        """
        Set one or more coord units at once.

        Parameters
        ----------
        *args : str(s)
            The list of units to apply to the set of coordinates (they must be given according to the coordinate's name
            alphabetical order.
        **kwargs
            Keyword attribution of the units. The keys must be valid names among the coordinate's name list. This
            is the recommended way to set units as this will be less prone to errors.
        force : bool, optional, default=False
            Whether or not the new units must be compatible with the current units. See the `Coord`.`to` method.

        Notes
        -----
        If the args are not named, then the attributions are made in coordinate's name alphabetical order :
        e.g, the first units will be for the `x` coordinates, the second for the `y`, etc.
        """
        force = kwargs.pop("force", False)

        if len(args) == 1 and is_sequence(args[0]):
            args = args[0]

        for i, item in enumerate(args):
            if not isinstance(self[i], CoordSet):
                self[i].to(item, force=force, inplace=True)
            else:
                if is_sequence(item):
                    for j, v in enumerate(self[i]):
                        v.to(item[j], force=force, inplace=True)

        for k, item in kwargs.items():
            self[k].to(item, force=force, inplace=True)
Example #2
0
    def set_titles(self, *args, **kwargs):
        """
        Set one or more coord title at once.

        Parameters
        ----------
        args : str(s)
            The list of titles to apply to the set of coordinates (they must be given according to the coordinate's name
            alphabetical order.
        **kwargs
            Keyword attribution of the titles. The keys must be valid names among the coordinate's name list. This
            is the recommended way to set titles as this will be less prone to errors.

        Notes
        -----
        If the args are not named, then the attributions are made in coordinate's  name alphabetical order :
        e.g, the first title will be for the `x` coordinates, the second for the `y`, etc.
        """
        if len(args) == 1 and (is_sequence(args[0])
                               or isinstance(args[0], CoordSet)):
            args = args[0]

        for i, item in enumerate(args):
            if not isinstance(self[i], CoordSet):
                self[i].title = item
            else:
                if is_sequence(item):
                    for j, v in enumerate(self[i]):
                        v.title = item[j]

        for k, item in kwargs.items():
            self[k].title = item
Example #3
0
    def set(self, *args, **kwargs):
        """
        Set one or more coordinates in the current CoordSet.

        Parameters
        ----------
        *args
        **kwargs

        Returns
        -------
        """
        if not args and not kwargs:
            return

        if len(args) == 1 and (is_sequence(args[0])
                               or isinstance(args[0], CoordSet)):
            args = args[0]

        if isinstance(args, CoordSet):
            kwargs.update(args.to_dict())
            args = ()

        if args:
            self._coords = []  # reset

        for i, item in enumerate(args[::-1]):
            item.name = self.available_names.pop()
            self._append(item)

        for k, item in kwargs.items():
            if isinstance(item, CoordSet):
                # try to keep this parameter to True!
                item._is_same_dim = True
            self[k] = item
Example #4
0
 def __call__(self, *args, **kwargs):
     # allow the following syntax: coords(), coords(0,2) or
     coords = []
     axis = kwargs.get("axis", None)
     if args:
         for idx in args:
             coords.append(self[idx])
     elif axis is not None:
         if not is_sequence(axis):
             axis = [axis]
         for i in axis:
             coords.append(self[i])
     else:
         coords = self._coords
     if len(coords) == 1:
         return coords[0]
     else:
         return CoordSet(*coords)
Example #5
0
def multiplot(
    datasets=[],
    labels=[],
    nrow=1,
    ncol=1,
    method="stack",
    sharex=False,
    sharey=False,
    sharez=False,
    colorbar=False,
    suptitle=None,
    suptitle_color="k",
    mpl_event=True,
    **kwargs
):
    """
    Generate a figure with multiple axes arranged in array (n rows, n columns).

    Parameters
    ----------
    datasets : nddataset or list of nddataset
        Datasets to plot.
    labels : list of str
        The labels that will be used as title of each axes.
    method : str, default to `map` for 2D and `lines` for 1D data
        Type of plot to draw in all axes (`lines` , `scatter` , `stack` , `map`
        ,`image` or `with_transposed`).
    nrows, ncols : int, default=1
        Number of rows/cols of the subplot grid. ncol*nrow must be equal
        to the number of datasets to plot.
    sharex, sharey : bool or {'none', 'all', 'row', 'col'}, default=False
        Controls sharing of properties among x (`sharex`) or y (`sharey`)
        axes::

        - True or 'all' : x- or y-axis will be shared among all subplots.
        - False or 'none' : each subplot x- or y-axis will be independent.
        - 'row' : each subplot row will share an x- or y-axis.
        - 'col' : each subplot column will share an x- or y-axis.

        When subplots have a shared x-axis along a column, only the x tick
        labels of the bottom subplot are visible.  Similarly, when
        subplots have a shared y-axis along a row, only the y tick labels
        of the first column subplot are visible.
    sharez : bool or {'none', 'all', 'row', 'col'}, default=False
        Equivalent to sharey for 1D plot.
        for 2D plot, z is the intensity axis (i.e., contour levels for maps or
        the vertical axis for stack plot), y is the third axis.
    figsize : 2-tuple of floats
        ``(width, height)`` tuple in inches.
    dpi : float
        Dots per inch
    facecolor : color
        The figure patch facecolor; defaults to rc ``figure.facecolor``.
    edgecolor : color
        The figure patch edge color; defaults to rc ``figure.edgecolor``.
    linewidth : float
        The figure patch edge linewidth; the default linewidth of the frame.
    frameon : bool
        If `False` , suppress drawing the figure frame.
    left : float in the [0-1] interval
        The left side of the subplots of the figure.
    right : float in the [0-1] interval
        The right side of the subplots of the figure.
    bottom : float in the [0-1] interval
        The bottom of the subplots of the figure.
    top : float in the [0-1] interval
        The top of the subplots of the figure.
    wspace : float in the [0-1] interval
        The amount of width reserved for blank space between subplots,
        expressed as a fraction of the average axis width.
    hspace : float in the [0-1] interval
        The amount of height reserved for white space between subplots,
        expressed as a fraction of the average axis height.
    suptitle : str
        Title of the figure to display on top.
    suptitle_color : color
        Color of the subtitles
    """

    # some basic checking
    # ------------------------------------------------------------------------

    show_transposed = False
    if method in "with_transposed":
        show_transposed = True
        method = "stack"
        nrow = 2
        ncol = 1
        datasets = [datasets, datasets]  # we need to datasets
        sharez = True

    single = False
    if not is_sequence(datasets):
        single = True
        datasets = list([datasets])  # make a list

    if len(datasets) < nrow * ncol and not show_transposed:
        # not enough datasets given in this list.
        raise ValueError("Not enough datasets given in this list")

    # if labels and len(labels) != len(datasets):
    #     # not enough labels given in this list.
    #     raise ValueError('Not enough labels given in this list')

    if nrow == ncol and nrow == 1 and not show_transposed and single:
        # obviously a single plot, return it
        return datasets[0].plot(**kwargs)
    elif nrow * ncol < len(datasets):
        nrow = ncol = len(datasets) // 2
        if nrow * ncol < len(datasets):
            ncol += 1

    ndims = set([dataset.ndim for dataset in datasets])
    if len(ndims) > 1:
        raise NotImplementedError("mixed dataset shape.")
    ndim = list(ndims)[0]

    # create the subplots and plot the ndarrays
    # ------------------------------------------------------------------------
    mpl.rcParams["figure.autolayout"] = False

    figsize = kwargs.pop("figsize", None)
    dpi = kwargs.pop("dpi", 150)

    fig = kwargs.pop("fig", None)
    if fig is None:
        fig = plt.figure(figsize=figsize, dpi=dpi)
    else:
        fig.clf()
        fig.set_size_inches(*figsize)

    fig.rcParams = plt.rcParams.copy()  # save params used for this figure

    if suptitle is not None:
        fig.suptitle(suptitle, color=suptitle_color)

    # axes is dictionary with keys such as 'axe12', where  the fist number
    # is the row and the second the column
    axes = {}

    # limits
    xlims = []
    ylims = []
    zlims = []

    if sharex not in [None, True, False, "all", "col"]:
        raise ValueError(
            "invalid option for sharex. Should be"
            " among (None, False, True, 'all' or 'col')"
        )

    if sharex:
        sharex = "all"

    if ndim == 1:
        sharez = False

    textsharey = "sharey"
    textsharez = "sharez"
    if method in ["stack"]:
        sharez, sharey = sharey, sharez  # we echange them
        zlims, ylims = ylims, zlims
        # for our internal needs as only sharex and sharey are recognized by
        # matplotlib subplots
        textsharey = "sharez"
        textsharez = "sharey"

    if sharey not in [None, False, True, "all", "col"]:
        raise ValueError(
            "invalid option for {}. Should be"
            " among (None, False, True, 'all' or 'row')".format(textsharey)
        )

    if sharez not in [None, False, True, "all", "col", "row"]:
        raise ValueError(
            "invalid option for {}. Should be"
            " among (None, False, True, "
            "'all', 'row' or 'col')".format(textsharez)
        )

    if sharey:
        sharey = "all"
    if sharez:
        sharez = "all"

    for irow in range(nrow):
        for icol in range(ncol):

            idx = irow * ncol + icol
            dataset = datasets[idx]
            try:
                label = labels[idx]
            except Exception:
                label = ""

            _sharex = None
            _sharey = None
            _sharez = None
            # on the type of the plot and
            if (
                (irow == icol and irow == 0)
                or (sharex == "col" and irow == 0)
                or (sharey == "row" and icol == 0)
            ):
                ax = _Axes(fig, nrow, ncol, irow * ncol + icol + 1)
                ax = fig.add_subplot(ax)

            else:
                if sharex == "all":
                    _sharex = axes["axe11"]
                elif sharex == "col":
                    _sharex = axes["axe1{}".format(icol + 1)]

                if sharey == "all":
                    _sharey = axes["axe11"]
                elif sharey == "row":
                    _sharey = axes["axe{}1".format(irow + 1)]

                # in the last dimension
                if sharez == "all":
                    _sharez = axes["axe11"]
                elif sharez == "row":
                    _sharez = axes["axe{}1".format(irow + 1)]
                elif sharez == "col":
                    _sharez = axes["axe1{}".format(icol + 1)]

                ax = _Axes(fig, nrow, ncol, idx + 1, sharex=_sharex, sharey=_sharey)
                ax = fig.add_subplot(ax)

            ax._sharez = _sharez  # we add a new share info to the ax.
            # which will be useful for the interactive masks

            ax.name = "axe{}{}".format(irow + 1, icol + 1)
            axes[ax.name] = ax
            if icol > 0 and sharey:
                # hide the redondant ticklabels on left side of interior figures
                plt.setp(axes[ax.name].get_yticklabels(), visible=False)
                axes[ax.name].yaxis.set_tick_params(
                    which="both", labelleft=False, labelright=False
                )
                axes[ax.name].yaxis.offsetText.set_visible(False)
            if irow < nrow - 1 and sharex:
                # hide the bottom ticklabels of interior rows
                plt.setp(axes[ax.name].get_xticklabels(), visible=False)
                axes[ax.name].xaxis.set_tick_params(
                    which="both", labelbottom=False, labeltop=False
                )
                axes[ax.name].xaxis.offsetText.set_visible(False)

            if show_transposed and irow == 1:
                transposed = True
            else:
                transposed = False

            dataset.plot(
                method=method,
                ax=ax,
                clear=False,
                autolayout=False,
                colorbar=colorbar,
                data_transposed=transposed,
                **kwargs
            )

            ax.set_title(label, fontsize=8)
            if sharex and irow < nrow - 1:
                ax.xaxis.label.set_visible(False)
            if sharey and icol > 0:
                ax.yaxis.label.set_visible(False)

            xlims.append(ax.get_xlim())
            ylims.append(ax.get_ylim())
            xrev = (ax.get_xlim()[1] - ax.get_xlim()[0]) < 0
            # yrev = (ax.get_ylim()[1] - ax.get_ylim()[0]) < 0

    # TODO: add a common color bar (set vmin and vmax using zlims)

    amp = np.ptp(np.array(ylims))
    ylim = [np.min(np.array(ylims) - amp * 0.01), np.max(np.array(ylims)) + amp * 0.01]
    for ax in axes.values():
        ax.set_ylim(ylim)
    # if yrev:
    #    ylim = ylim[::-1]
    # amp = np.ptp(np.array(xlims))

    if not show_transposed:
        xlim = [np.min(np.array(xlims)), np.max(np.array(xlims))]
        if xrev:
            xlim = xlim[::-1]
        for ax in axes.values():
            ax.set_xlim(xlim)

    def do_tight_layout(fig, axes, suptitle, **kwargs):

        # tight_layout
        renderer = get_renderer(fig)
        axeslist = list(axes.values())
        subplots_list = list(get_subplotspec_list(axeslist))
        kw = get_tight_layout_figure(
            fig,
            axeslist,
            subplots_list,
            renderer,
            # pad=1.1,
            h_pad=0,
            w_pad=0,
            rect=None,
        )

        left = kwargs.get("left", kw["left"])
        bottom = kwargs.get("bottom", kw["bottom"])
        right = kwargs.get("right", kw["right"])
        top = kw["top"]
        if suptitle:
            top = top * 0.95
        top = kwargs.get("top", top)
        ws = kwargs.get("wspace", kw.get("wspace", 0) * 1.1)
        hs = kwargs.get("hspace", kw.get("hspace", 0) * 1.1)

        plt.subplots_adjust(
            left=left, bottom=bottom, right=right, top=top, wspace=ws, hspace=hs
        )

    do_tight_layout(fig, axes, suptitle, **kwargs)

    if mpl_event:
        # make an event that will trigger subplot adjust each time the mouse leave
        # or enter the axes or figure
        def _onenter(event):
            do_tight_layout(fig, axes, suptitle, **kwargs)
            fig.canvas.draw()

        fig.canvas.mpl_connect("axes_enter_event", _onenter)
        fig.canvas.mpl_connect("axes_leave_event", _onenter)
        fig.canvas.mpl_connect("figure_enter_event", _onenter)
        fig.canvas.mpl_connect("figure_leave_event", _onenter)

    return axes
Example #6
0
    def __init__(self, *coords, **kwargs):

        self._copy = kwargs.pop("copy", True)
        self._sorted = kwargs.pop("sorted", True)

        keepnames = kwargs.pop("keepnames", False)
        # if keepnames is false and the names of the dimensions are not passed in kwargs, then use dims if not none
        dims = kwargs.pop("dims", None)

        self.name = kwargs.pop("name", None)

        # initialise the coordinate list
        self._coords = []

        # First evaluate passed args
        # --------------------------

        # some cleaning
        if coords:

            if all([(isinstance(coords[i],
                                (np.ndarray, NDArray, list, CoordSet))
                     or coords[i] is None) for i in range(len(coords))]):
                # Any instance of a NDArray can be accepted as coordinates for a dimension.
                # If an instance of CoordSet is found, this means that all
                # coordinates in this set describe the same axis
                coords = tuple(coords)

            elif is_sequence(coords) and len(coords) == 1:
                # if isinstance(coords[0], list):
                #     coords = (CoordSet(*coords[0], sorted=False),)
                # else:
                coords = coords[0]

                if isinstance(coords, dict):
                    # we have passed a dict, postpone to the kwargs evaluation process
                    kwargs.update(coords)
                    coords = None

            else:
                raise ValueError("Did not understand the inputs")

        # now store the args coordinates in self._coords (validation is fired when this attribute is set)
        if coords:
            for coord in coords[::-1]:  # we fill from the end of the list
                # (in reverse order) because by convention when the
                # names are not specified, the order of the
                # coords follow the order of dims.
                if not isinstance(coord, CoordSet):
                    if isinstance(coord, list):
                        coord = CoordSet(*coord[::-1], sorted=False)
                    elif not isinstance(coord, LinearCoord):  # else
                        coord = Coord(coord, copy=True)
                else:
                    coord = cpy.deepcopy(coord)

                if not keepnames:
                    if dims is None:
                        # take the last available name of available names list
                        coord.name = self.available_names.pop(-1)
                    else:
                        # use the provided list of dims
                        coord.name = dims.pop(-1)

                self._append(coord)  # append the coord (but instead of append,
                # use assignation -in _append - to fire the validation process )

        # now evaluate keywords argument
        # ------------------------------

        for key, coord in list(kwargs.items())[:]:
            # remove the already used kwargs (Fix: deprecation warning in Traitlets - all args, kwargs must be used)
            del kwargs[key]

            # prepare values to be either Coord, LinearCoord or CoordSet
            if isinstance(coord, (list, tuple)):
                coord = CoordSet(
                    *coord, sorted=False
                )  # make sure in this case it becomes a CoordSet instance

            elif isinstance(coord, np.ndarray) or coord is None:
                coord = Coord(
                    coord, copy=True
                )  # make sure it's a Coord  # (even if it is None -> Coord(None)

            elif isinstance(coord, str) and coord in DEFAULT_DIM_NAME:
                # may be a reference to another coordinates (e.g. same coordinates for various dimensions)
                self._references[key] = coord  # store this reference
                continue

            # Populate the coords with coord and coord's name.
            if isinstance(coord,
                          (NDArray, Coord, LinearCoord, CoordSet)):  # NDArray,
                if key in self.available_names or (
                        len(key) == 2 and key.startswith("_")
                        and key[1] in list("123456789")):
                    # ok we can find it as a canonical name:
                    # this will overwrite any already defined coord value
                    # which means also that kwargs have priority over args
                    coord.name = key
                    self._append(coord)

                elif not self.is_empty and key in self.names:
                    # append when a coordinate with this name is already set in passed arg.
                    # replace it
                    idx = self.names.index(key)
                    coord.name = key
                    self._coords[idx] = coord

                else:
                    raise KeyError(
                        f"Probably an invalid key (`{key}`) for coordinates has been passed. "
                        f"Valid keys are among:{DEFAULT_DIM_NAME}")

            else:
                raise ValueError(
                    f"Probably an invalid type of coordinates has been passed: {key}:{coord} "
                )

        # store the item (validation will be performed)
        # self._coords = _coords

        # inform the parent about the update
        self._updated = True

        # set a notifier on the name traits name of each coordinates
        for coord in self._coords:
            if coord is not None:
                HasTraits.observe(coord, self._coords_update, "_name")

        # initialize the base class with the eventual remaining arguments
        super().__init__(**kwargs)
Example #7
0
    def __init__(self, *coords, **kwargs):
        """
        A collection of Coord objects for a NDArray object with validation.

        This object is an iterable containing a collection of Coord objects.

        Parameters
        ----------
        *coords : |NDarray|, |NDArray| subclass or |CoordSet| sequence of objects.
            If an instance of CoordSet is found, instead of an array, this means
            that all coordinates in this coords describe the same axis.
            It is assumed that the coordinates are passed in the order of the
            dimensions of a nD numpy array (
            `row-major <https://docs.scipy.org/doc/numpy-1.14.1/glossary.html#term-row-major>`_
            order), i.e., for a 3d object : 'z', 'y', 'x'.
        **kwargs: dict
            See other parameters.

        Other Parameters
        ----------------
        x : |NDarray|, |NDArray| subclass or |CoordSet|
            A single coordinate associated to the 'x'-dimension.
            If a coord was already passed in the argument, this will overwrite
            the previous. It is thus not recommended to simultaneously use
            both way to initialize the coordinates to avoid such conflicts.
        y, z, u, ... : |NDarray|, |NDArray| subclass or |CoordSet|
            Same as `x` for the others dimensions.
        dims : list of string, optional
            Names of the dims to use corresponding to the coordinates. If not given, standard names are used: x, y, ...

        See Also
        --------
        Coord : Explicit coordinates object.
        LinearCoord : Implicit coordinates object.
        NDDataset: The main object of SpectroChempy which makes use of CoordSet.

        Examples
        --------
        >>> from spectrochempy import Coord, CoordSet

        Define 4 coordinates, with two for the same dimension

        >>> coord0 = Coord.linspace(10., 100., 5, units='m', title='distance')
        >>> coord1 = Coord.linspace(20., 25., 4, units='K', title='temperature')
        >>> coord1b = Coord.linspace(1., 10., 4, units='millitesla', title='magnetic field')
        >>> coord2 = Coord.linspace(0., 1000., 6, units='hour', title='elapsed time')

        Now create a coordset

        >>> cs = CoordSet(t=coord0, u=coord2, v=[coord1, coord1b])

        Display some coordinates

        >>> cs.u
        Coord: [float64] hr (size: 6)

        >>> cs.v
        CoordSet: [_1:temperature, _2:magnetic field]

        >>> cs.v_1
        Coord: [float64] K (size: 4)
        """

        self._copy = kwargs.pop('copy', True)
        self._sorted = kwargs.pop('sorted', True)

        keepnames = kwargs.pop('keepnames', False)
        # if keepnames is false and the names of the dimensions are not passed in kwargs, then use dims if not none
        dims = kwargs.pop('dims', None)

        self.name = kwargs.pop('name', None)

        # initialise the coordinate list
        self._coords = []

        # First evaluate passed args
        # --------------------------

        # some cleaning
        if coords:

            if all([(isinstance(coords[i],
                                (np.ndarray, NDArray, list, CoordSet))
                     or coords[i] is None) for i in range(len(coords))]):
                # Any instance of a NDArray can be accepted as coordinates for a dimension.
                # If an instance of CoordSet is found, this means that all
                # coordinates in this set describe the same axis
                coords = tuple(coords)

            elif is_sequence(coords) and len(coords) == 1:
                # if isinstance(coords[0], list):
                #     coords = (CoordSet(*coords[0], sorted=False),)
                # else:
                coords = coords[0]

                if isinstance(coords, dict):
                    # we have passed a dict, postpone to the kwargs evaluation process
                    kwargs.update(coords)
                    coords = None

            else:
                raise ValueError('Did not understand the inputs')

        # now store the args coordinates in self._coords (validation is fired when this attribute is set)
        if coords:
            for coord in coords[::-1]:  # we fill from the end of the list
                # (in reverse order) because by convention when the
                # names are not specified, the order of the
                # coords follow the order of dims.
                if not isinstance(coord, CoordSet):
                    if isinstance(coord, list):
                        coord = CoordSet(*coord, sorted=False)
                    elif not isinstance(coord, LinearCoord):  # else
                        coord = Coord(coord, copy=True)
                else:
                    coord = cpy.deepcopy(coord)

                if not keepnames:
                    if dims is None:
                        # take the last available name of available names list
                        coord.name = self.available_names.pop(-1)
                    else:
                        # use the provided list of dims
                        coord.name = dims.pop(-1)

                self._append(coord)  # append the coord (but instead of append,
                # use assignation -in _append - to fire the validation process )

        # now evaluate keywords argument
        # ------------------------------

        for key, coord in list(kwargs.items())[:]:
            # remove the already used kwargs (Fix: deprecation warning in Traitlets - all args, kwargs must be used)
            del kwargs[key]

            # prepare values to be either Coord, LinearCoord or CoordSet
            if isinstance(coord, (list, tuple)):
                coord = CoordSet(
                    *coord, sorted=False
                )  # make sure in this case it becomes a CoordSet instance

            elif isinstance(coord, np.ndarray) or coord is None:
                coord = Coord(
                    coord, copy=True
                )  # make sure it's a Coord  # (even if it is None -> Coord(None)

            elif isinstance(coord, str) and coord in DEFAULT_DIM_NAME:
                # may be a reference to another coordinates (e.g. same coordinates for various dimensions)
                self._references[key] = coord  # store this reference
                continue

            # Populate the coords with coord and coord's name.
            if isinstance(coord,
                          (NDArray, Coord, LinearCoord, CoordSet)):  # NDArray,
                if key in self.available_names or (
                        len(key) == 2 and key.startswith('_')
                        and key[1] in list("123456789")):
                    # ok we can find it as a canonical name:
                    # this will overwrite any already defined coord value
                    # which means also that kwargs have priority over args
                    coord.name = key
                    self._append(coord)

                elif not self.is_empty and key in self.names:
                    # append when a coordinate with this name is already set in passed arg.
                    # replace it
                    idx = self.names.index(key)
                    coord.name = key
                    self._coords[idx] = coord

                else:
                    raise KeyError(
                        f'Probably an invalid key (`{key}`) for coordinates has been passed. '
                        f'Valid keys are among:{DEFAULT_DIM_NAME}')

            else:
                raise ValueError(
                    f'Probably an invalid type of coordinates has been passed: {key}:{coord} '
                )

        # store the item (validation will be performed)
        # self._coords = _coords

        # inform the parent about the update
        self._updated = True

        # set a notifier on the name traits name of each coordinates
        for coord in self._coords:
            if coord is not None:
                HasTraits.observe(coord, self._coords_update, '_name')

        # initialize the base class with the eventual remaining arguments
        super().__init__(**kwargs)
Example #8
0
def plot_multiple(datasets, method="scatter", pen=True, labels=None, **kwargs):
    """
    Plot a series of 1D datasets as a scatter plot with optional lines between markers.

    Parameters
    ----------
    datasets : a list of ndatasets
    method : str among [scatter, pen]
    pen : bool, optional, default: True
        If method is scatter, this flag tells to draw also the lines
        between the marks.
    labels : a list of str, optional
        Labels used for the legend.
    **kwargs
        Other parameters that will be passed to the plot1D function.

    Other Parameters
    ----------------
    {0}

    See Also
    --------
    plot_1D
    plot_pen
    plot_scatter
    plot_bar
    plot_scatter_pen
    """
    if not is_sequence(datasets):
        # we need a sequence. Else it is a single plot.
        return datasets.plot(**kwargs)

    if not is_sequence(labels) or len(labels) != len(datasets):
        # we need a sequence of labels of same length as datasets
        raise ValueError(
            "the list of labels must be of same length " "as the datasets list"
        )

    for dataset in datasets:
        if dataset._squeeze_ndim > 1:
            raise NotImplementedError(
                "plot multiple is designed to work on "
                "1D dataset only. you may achieved "
                "several plots with "
                "the `clear=False` parameter as a work "
                "around "
                "solution"
            )

    # do not save during this plots, nor apply any commands
    # we will make this when all plots will be done

    output = kwargs.get("output", None)
    kwargs["output"] = None
    commands = kwargs.get("commands", [])
    kwargs["commands"] = []
    clear = kwargs.pop("clear", True)
    legend = kwargs.pop(
        "legend", None
    )  # remove 'legend' from kwargs before calling plot
    # else it will generate a conflict

    for s in datasets:  # , colors, markers):

        ax = s.plot(
            method=method,
            pen=pen,
            marker="AUTO",
            color="AUTO",
            ls="AUTO",
            clear=clear,
            **kwargs
        )
        clear = False  # clear=False is necessary for the next plot to say
        # that we will plot on the same figure

    # scale all plots
    if legend is not None:
        _ = ax.legend(
            ax.lines,
            labels,
            shadow=True,
            loc=legend,
            frameon=True,
            facecolor="lightyellow",
        )

    # now we can output the final figure
    kw = {"output": output, "commands": commands}
    datasets[0]._plot_resume(datasets[-1], **kw)

    return ax