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)
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
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
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)
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
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)
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)
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