class Anim: def __init__(self, eddy, intern=False, sleep_event=0.1, **kwargs): self.eddy = eddy x_name, y_name = eddy.intern(intern) self.t, self.x, self.y = eddy.time, eddy[x_name], eddy[y_name] self.pause = False self.period = self.eddy.period self.sleep_event = sleep_event self.setup(**kwargs) def setup(self, cmap="jet", nb_step=25, figsize=(8, 6)): cmap = pyplot.get_cmap(cmap) self.colors = cmap(arange(nb_step + 1) / nb_step) self.nb_step = nb_step x_min, x_max = self.x.min(), self.x.max() d_x = x_max - x_min x_min -= 0.05 * d_x x_max += 0.05 * d_x y_min, y_max = self.y.min(), self.y.max() d_y = y_max - y_min y_min -= 0.05 * d_y y_max += 0.05 * d_y # plot self.fig = pyplot.figure(figsize=figsize) t0, t1 = self.period self.fig.suptitle(f"{t0} -> {t1}") self.ax = self.fig.add_axes((0.05, 0.05, 0.9, 0.9)) self.ax.set_xlim(x_min, x_max), self.ax.set_ylim(y_min, y_max) self.ax.set_aspect("equal") self.ax.grid() # init mappable self.txt = self.ax.text(x_min + 0.05 * d_x, y_min + 0.05 * d_y, "", zorder=10) self.contour = LineCollection([], zorder=1) self.ax.add_collection(self.contour) self.fig.canvas.draw() self.fig.canvas.mpl_connect("key_press_event", self.keyboard) def show(self, infinity_loop=False): pyplot.show(block=False) # save background for future bliting self.fig.canvas.draw() self.bg_cache = self.fig.canvas.copy_from_bbox(self.ax.bbox) loop = True t0, t1 = self.period while loop: self.segs = list() self.now = t0 while True: dt = self.sleep_event if not self.pause: d0 = datetime.now() self.next() dt_draw = (datetime.now() - d0).total_seconds() dt = self.sleep_event - dt_draw if dt < 0: # self.sleep_event = dt_draw * 1.01 dt = 1e-10 self.fig.canvas.start_event_loop(dt) if self.now > t1: break if infinity_loop: self.fig.canvas.start_event_loop(0.5) else: loop = False def next(self): self.now += 1 return self.draw_contour() def prev(self): self.now -= 1 return self.draw_contour() def draw_contour(self): # t0, t1 = self.period # select contour for this time step m = self.t == self.now self.ax.figure.canvas.restore_region(self.bg_cache) if m.sum(): self.segs.append( create_vertice(flatten_line_matrix(self.x[m]), flatten_line_matrix(self.y[m]))) else: self.segs.append(empty((0, 2))) self.contour.set_paths(self.segs) self.contour.set_color(self.colors[-len(self.segs):]) self.contour.set_lw(arange(len(self.segs)) / len(self.segs) * 2.5) self.txt.set_text(f"{self.now} - {1/self.sleep_event:.0f} frame/s") self.ax.draw_artist(self.contour) self.ax.draw_artist(self.txt) # Remove first segment to keep only T contour if len(self.segs) > self.nb_step: self.segs.pop(0) # paint updated artist self.ax.figure.canvas.blit(self.ax.bbox) def keyboard(self, event): if event.key == "escape": exit() elif event.key == " ": self.pause = not self.pause elif event.key == "+": self.sleep_event *= 0.9 elif event.key == "-": self.sleep_event *= 1.1 elif event.key == "right" and self.pause: self.next() elif event.key == "left" and self.pause: self.segs.pop(-1) self.segs.pop(-1) self.prev()
class Anim: def __init__( self, eddy, intern=False, sleep_event=0.1, graphic_information=False, **kwargs ): self.eddy = eddy x_name, y_name = eddy.intern(intern) self.t, self.x, self.y = eddy.time, eddy[x_name], eddy[y_name] self.x_core, self.y_core, self.track = eddy["lon"], eddy["lat"], eddy["track"] self.graphic_informations = graphic_information self.pause = False self.period = self.eddy.period self.sleep_event = sleep_event self.mappables = list() self.field_color = None self.field_txt = None self.time_field = False self.setup(**kwargs) def setup( self, cmap="jet", lut=None, field_color="time", field_txt="track", range_color=(None, None), nb_step=25, figsize=(8, 6), **kwargs, ): self.field_color = self.eddy[field_color].astype("f4") self.field_txt = self.eddy[field_txt] rg = range_color if rg[0] is None and rg[1] is None and field_color == "time": self.time_field = True else: rg = ( self.field_color.min() if rg[0] is None else rg[0], self.field_color.max() if rg[1] is None else rg[1], ) self.field_color = (self.field_color - rg[0]) / (rg[1] - rg[0]) self.colors = pyplot.get_cmap(cmap, lut=lut) self.nb_step = nb_step x_min, x_max = self.x_core.min() - 2, self.x_core.max() + 2 d_x = x_max - x_min y_min, y_max = self.y_core.min() - 2, self.y_core.max() + 2 d_y = y_max - y_min # plot self.fig = pyplot.figure(figsize=figsize, **kwargs) t0, t1 = self.period self.fig.suptitle(f"{t0} -> {t1}") self.ax = self.fig.add_axes((0.05, 0.05, 0.9, 0.9), projection="full_axes") self.ax.set_xlim(x_min, x_max), self.ax.set_ylim(y_min, y_max) self.ax.set_aspect("equal") self.ax.grid() # init mappable self.txt = self.ax.text(x_min + 0.05 * d_x, y_min + 0.05 * d_y, "", zorder=10) self.segs = list() self.t_segs = list() self.c_segs = list() self.contour = LineCollection([], zorder=1) self.ax.add_collection(self.contour) self.fig.canvas.draw() self.fig.canvas.mpl_connect("key_press_event", self.keyboard) self.fig.canvas.mpl_connect("resize_event", self.reset_bliting) def reset_bliting(self, event): self.contour.set_visible(False) self.txt.set_visible(False) for m in self.mappables: m.set_visible(False) self.fig.canvas.draw() self.bg_cache = self.fig.canvas.copy_from_bbox(self.ax.bbox) self.contour.set_visible(True) self.txt.set_visible(True) for m in self.mappables: m.set_visible(True) def show(self, infinity_loop=False): pyplot.show(block=False) # save background for future bliting self.fig.canvas.draw() self.bg_cache = self.fig.canvas.copy_from_bbox(self.ax.bbox) loop = True t0, t1 = self.period while loop: self.now = t0 while True: dt = self.sleep_event if not self.pause: d0 = datetime.now() self.next() dt_draw = (datetime.now() - d0).total_seconds() dt = self.sleep_event - dt_draw if dt < 0: # self.sleep_event = dt_draw * 1.01 dt = 1e-10 if dt == 0: dt = 1e-10 self.fig.canvas.start_event_loop(dt) if self.now > t1: break if infinity_loop: self.fig.canvas.start_event_loop(0.5) else: loop = False def next(self): self.now += 1 return self.draw_contour() def prev(self): self.now -= 1 return self.draw_contour() def func_animation(self, frame): while self.mappables: self.mappables.pop().remove() self.now = frame self.update() artists = [self.contour, self.txt] artists.extend(self.mappables) return artists def update(self): m = self.t == self.now if m.sum(): segs = list() t = list() c = list() for i in where(m)[0]: segs.append(create_vertice(self.x[i], self.y[i])) c.append(self.field_color[i]) t.append(self.now) self.segs.append(segs) self.c_segs.append(c) self.t_segs.append(t) self.contour.set_paths(chain(*self.segs)) if self.time_field: self.contour.set_color( self.colors( [ (self.nb_step - self.now + i) / self.nb_step for i in chain(*self.c_segs) ] ) ) else: self.contour.set_color(self.colors(list(chain(*self.c_segs)))) # linewidth will be link to time delay self.contour.set_lw( [ (1 - (self.now - i) / self.nb_step) * 2.5 if i <= self.now else 0 for i in chain(*self.t_segs) ] ) # Update date txt and info txt = f"{(timedelta(int(self.now)) + datetime(1950,1,1)).strftime('%Y/%m/%d')}" if self.graphic_informations: txt += f"- {1/self.sleep_event:.0f} frame/s" self.txt.set_text(txt) # Update id txt for i in where(m)[0]: mappable = self.ax.text( self.x_core[i], self.y_core[i], self.field_txt[i], fontsize=12, fontweight="demibold", ) self.mappables.append(mappable) self.ax.draw_artist(mappable) self.ax.draw_artist(self.contour) self.ax.draw_artist(self.txt) # Remove first segment to keep only T contour if len(self.segs) > self.nb_step: self.segs.pop(0) self.t_segs.pop(0) self.c_segs.pop(0) def draw_contour(self): # select contour for this time step while self.mappables: self.mappables.pop().remove() self.ax.figure.canvas.restore_region(self.bg_cache) self.update() # paint updated artist self.ax.figure.canvas.blit(self.ax.bbox) def keyboard(self, event): if event.key == "escape": exit() elif event.key == " ": self.pause = not self.pause elif event.key == "+": self.sleep_event *= 0.9 elif event.key == "-": self.sleep_event *= 1.1 elif event.key == "right" and self.pause: self.next() elif event.key == "left" and self.pause: # we remove 2 step to add 1 so we rewind of only one self.segs.pop(-1) self.segs.pop(-1) self.t_segs.pop(-1) self.t_segs.pop(-1) self.c_segs.pop(-1) self.c_segs.pop(-1) self.prev()
def location_plot(data, x=None, y=None, z=None, var=None, dhid=None, catdata=None, allcats=True, cbar=True, cbar_label=None, catdict=None, cmap=None, cax=None, vlim=None, title=None, plot_collar=True, collar_marker='x', collar_offset=0, lw=None, xlabel=None, ylabel=None, unit=None, griddef=None, slice_number=None, orient='xy', slicetol=None, xlim=None, ylim=None, ax=None, figsize=None, s=None, marker='o', rotateticks=None, sigfigs=3, grid=None, axis_xy=None, aspect=None, plot_style=None, custom_style=None, output_file=None, out_kws=None, return_cbar=False, return_plot=False, **kwargs): """ location_plot displays scattered data on a 2-D XY plot. To plot gridded data with or without scattered data, please see :func:`gs.slice_plot()<pygeostat.plotting.slice_plot>`. The only required parameter is ``data`` if it is a :class:`gs.DataFile <pygeostat.data.data.DataFile>` that contains the necessary coordinate column headers, data, and if required, a pointer to a valid :class:`gs.GridDef <pygeostat.data.grid_definition.GridDef>` class. All other parameters are optional. If ``data`` is a :class:`gs.DataFile <pygeostat.data.data.DataFile>` class and does not contain all the required parameters or if it is a long-form table, the following parameters will need to be pass are needed: ``x``, ``y``, ``z``, and ``griddef``. The three coordinate parameters may not be needed depending on what ``orient`` is set to and of course if the dataset is 2-D or 3-D. The parameter ``griddef`` is required if ``slicetol`` or `` slice_number`` is used. If parameter ``slice_number`` and ``slicetol`` is not set then the default slice tolerance is half the cell width. If a negative ``slicetol`` is passed or slice_number is set to None then all data will be plotted. ``slicetol`` is based on coordinate units. The values used to bound the data (i.e., vmin and vmax) are automatically calculated by default. These values are determined based on the number of significant figures and the sliced data; depending on data and the precision specified, scientific notation may be used for the colorbar tick lables. When point data shares the same colormap as the gridded data, the points displayed are integrated into the above calculation. Please review the documentation of the :func:`gs.set_style() <pygeostat.plotting.set_style.set_style>` and :func:`gs.export_image() <pygeostat.plotting.export_image.export_image>` functions for details on their parameters so that their use in this function can be understood. Parameters: data (pd.DataFrame or gs.DataFile): data containing coordinates and (optionally) var x (str): Column header of x-coordinate. Required if the conditions discussed above are not met y (str): Column header of y-coordinate. Required if the conditions discussed above are not met z (str): Column header of z-coordinate. Required if the conditions discussed above are not met var (str): Column header of the variable to use to colormap the points. Can also be a list of or single permissible matplotlib colour(s). If None and data is a DataFile, based on DataFile.variables if len(DataFile.variables) == 1. Otherwise, based on Parameters['plotting.location_plot.c'] dhid (str): Column header of drill hole ID. catdata (bool): Force categorical data catdict (dict): Dictionary containing the enumerated IDs alphabetic equivalent, which is drawn from Parameters['data.catdict'] if None allcats (bool): ensures that if categorical data is being plotted and plotted on slices, that the categories will be the same color between slices if not all categories are present on each slice cbar (bool): Indicate if a colorbar should be plotted or not cbar_label (str): Colorbar title cmap (str): A matplotlib colormap object or a registered matplotlib or pygeostat colormap name. cax(Matplotlib.ImageGrid.cbar_axes): color axis, if a previously created one should be used vlim (float tuple): Data minimum and maximum values title (str): Title for the plot. If left to it's default value of ``None`` or is set to ``True``, a logical default title will be generated for 3-D data. Set to ``False`` if no title is desired. plot_collar (bool): Option to plot the collar if the orient is xz or yz and the dhid is provided/inferred collar_marker (str): One of the permissible matplotlib markers, like 'o', or '+'... and others. lw (float): Line width value if the orient is xz or yz and the dhid is provided/inferred. Because lines or plotted instead of points. xlabel (str): X-axis label ylabel (str): Y-axis label unit (str): Unit to place inside the axis label parentheses griddef (GridDef): A pygeostat GridDef class created using :class:`gs.GridDef <pygeostat.data.grid_definition.GridDef>`. Required if using the argument ``slicetol`` orient (str): Orientation to slice data. ``'xy'``, ``'xz'``, ``'yz'`` are t he only accepted values slice_number (int): Grid cell location along the axis not plotted to take the slice of data to plot. None will plot all data slicetol (float): Slice tolerance to plot point data (i.e. plot +/- ``slicetol`` from the center of the slice). Any negative value plots all data. Requires ``slice_number``. If a ``slice_number`` is passed and no ``slicetol`` is set, then the default will half the cell width based on the griddef. xlim (float tuple): X-axis limits. If None, based on data.griddef.extents(). If data.griddef is None, based on the limits of the data. ylim (float tuple): Y-axis limits. If None, based on data.griddef.extents(). If data.griddef is None, based on the limits of the data. ax (mpl.axis): Matplotlib axis to plot the figure figsize (tuple): Figure size (width, height) s (float): Size of location map markers marker (str): One of the permissible matplotlib markers, like 'o', or '+'... and others. rotateticks (bool tuple): Indicate if the axis tick labels should be rotated (x, y) sigfigs (int): Number of sigfigs to consider for the colorbar grid (bool): Plots the major grid lines if True. Based on Parameters['plotting.grid'] if None. axis_xy (bool): converts the axis to GSLIB-style axis visibility (only left and bottom visible) if axis_xy is True. Based on Parameters['plotting.axis_xy'] if None. aspect (str): Set a permissible aspect ratio of the image to pass to matplotlib. If None, it will be 'equal' if each axis is within 1/5 of the length of the other. Otherwise, it will be 'auto'. plot_style (str): Use a predefined set of matplotlib plotting parameters as specified by :class:`gs.GridDef <pygeostat.data.grid_definition.GridDef>`. Use ``False`` or ``None`` to turn it off custom_style (dict): Alter some of the predefined parameters in the ``plot_style`` selected. output_file (str): Output figure file name and location out_kws (dict): Optional dictionary of permissible keyword arguments to pass to :func:`gs.export_image() <pygeostat.plotting.export_image.export_image>` return_cbar (bool): Indicate if the colorbar axis should be returned. A tuple is returned with the first item being the axis object and the second the cbar object. return_plot (bool): Indicate if the plot from scatter should be returned. It can be used to create the colorbars required for subplotting with the ImageGrid() **kwargs: Optional permissible keyword arguments to pass to matplotlib's scatter function Returns: ax (ax): Matplotlib axis instance which contains the gridded figure Returns: cbar (cbar): Optional, default False. Matplotlib colorbar object **Examples:** A simple call: .. plot:: import pygeostat as gs data_file = gs.ExampleData('point3d_ind_mv') gs.location_plot(data_file) | A simple call using a variable to color the data: .. plot:: import pygeostat as gs data_file = gs.ExampleData('point3d_ind_mv') gs.location_plot(data_file, var = 'Phi') | Plotting along xz/yz orientation and use line plots based on drill hole id .. plot:: import pygeostat as gs # load data data_file = gs.ExampleData('point3d_ind_mv') gs.location_plot(data_file, var='Phi', orient='yz', aspect =5, plot_collar = True) | Location plot for a categorical variable .. plot:: import pygeostat as gs # load data data_file = gs.ExampleData('point3d_ind_mv') gs.location_plot(data_file, var='Lithofacies', orient='yz', aspect =5, plot_collar = True) | """ from .utils import (_spatial_labels, _spatial_pointdata, _spatial_slice, _format_tick_labels, setup_plot, _spatial_orient2fig, format_plot, _get_cmap, _spatial_aspect, get_contcbarargs) from ..pygeostat_parameters import Parameters from .export_image import export_image from ..data.data import DataFile from .cmaps import avail_palettes from matplotlib.collections import LineCollection # Handle dictionary defaults if not out_kws: out_kws = dict() # Infer dhid column if not provided drill_plot_format = False if dhid is None: if isinstance(data, DataFile): dhid = data.dh else: if not dhid in data.columns: raise ValueError( 'The provided drill hole id column, {} does not exist'.format( dhid)) # Handle var if var is None: if isinstance(data, DataFile): if data.variables is not None: if isinstance(data.variables, str): var = data.variables elif isinstance(data.cat, str): var = data.cat if var is None: var = Parameters['plotting.location_plot.c'] # Handle title if title is None: if var in data.columns: title = var # Determine the x, y, z and griddef with error checking data, x, y, z, griddef = _spatial_pointdata(data, orient, x, y, z, griddef) # Determine the data to plot based on grid def and slice info if dhid is None: pointx, pointy, pointvar = _spatial_slice(data, var, x, y, z, griddef, orient, slice_number, slicetol) else: pointx, pointy, pointvar, point_dh = _spatial_slice( data, var, x, y, z, griddef, orient, slice_number, slicetol, dhid) if pointx is None: if griddef is None: raise ValueError( 'At least two coordinates are required for the location_plot') else: print( 'Note: There is no data point detected within the provided slice number and/or slice tolerance' ) # Setup the color if pointvar is None: try: pointvar = mpl.colors.ColorConverter().to_rgb(var) if int(mpl.__version__.split('.')[0]) >= 3: pointvar = [pointvar] cbar = False except Exception: if griddef is None: raise ValueError( 'var is not a variable in the passed data and not a valid Matplotlib color {}' .format(var)) else: pointvar = [ (0.0, 0.0, 0.0) ] # Use a default color since there is no pint within the grid cbar = False # Get default colormaps or palettes. If there are Parameters['plotting.assumecat'] or less # unique data, assume categorical if isinstance(var, str): if var in data.columns: if allcats: alldata = data[var] else: alldata = pointvar ncat = len(np.unique(alldata[np.isfinite(alldata)])) else: ncat = None else: ncat = None cmap = None if catdata is not True: if cmap is None and catdata is None and ncat is not None and \ ncat < Parameters['plotting.assumecat']: catdata = True # Get palette from pygeostat elif cmap in avail_palettes: catdata = True elif ncat is not None and ncat <= Parameters[ 'plotting.assumecat'] and catdata is None: catdata = True else: catdata = False if cmap is None: cmap = _get_cmap(cmap, catdata, ncat) # Set-up categorical parameter if required if catdata: ticklabels = np.unique(alldata[np.isfinite(alldata)]).astype(int) if catdict is None: catdict = Parameters['data.catdict'] if isinstance(catdict, dict): if len(ticklabels) != len(catdict): # modify the cmap to store the colors as if cmap is generated from all cats cmap = _get_cmap(cmap, True, len(catdict)) cmap = mpl.colors.ListedColormap([ clr for cat, clr in zip(catdict, cmap.colors) if cat in ticklabels ]) vlim = (0, ncat) ticklocs = np.arange(ncat) + 0.5 if cmap: if isinstance(cmap, str): cmap = _get_cmap(cmap, catdata, ncat) dump = pointvar.copy() for i in range(ncat): dump[pointvar == ticklabels[i]] = i pointvar = dump if isinstance(catdict, dict): ticklabels = [catdict[cat] for cat in ticklabels] # Set-up some parameters if not catdata and cmap is not False and cbar: vlim, ticklocs, ticklabels = get_contcbarargs(pointvar, sigfigs, vlim) if vlim is None: vlim = (None, None) # Set-up plot if no axis is supplied using the ImageGrid method if required or the regular way fig, ax, cax = setup_plot(ax, cax=cax, cbar=cbar, figsize=figsize) if s is None: s = Parameters['plotting.location_plot.s'] # used for plot extents and get the two coordinates being plotted figx, figy, _ = _spatial_orient2fig(orient, x, y, z) if orient.lower() == 'xy' or not dhid: plot = ax.scatter(pointx, pointy, c=pointvar, cmap=cmap, vmin=vlim[0], vmax=vlim[1], s=s, marker=marker, **kwargs) else: # Plot lines if the dhid was inferred/provided and the orientation is xz or yz drill_plot_format = True data_temp = pd.DataFrame({ figx: pointx, figy: pointy, var: pointvar, dhid: point_dh }) grouped = data_temp.groupby(dhid, sort=False) if lw is None: lw = Parameters['plotting.location_plot.lw'] for _, group in grouped: if plot_collar: x_points = group[figx][group[figx].index.min()] y_points = group[figy][group[figy].index.min()] + collar_offset if var in data.columns: collar_var = group[var][group[var].index.min()] norm = plt.cm.colors.Normalize(*vlim) _cmap = plt.cm.get_cmap(cmap) collar_var = [_cmap(norm(collar_var))] plot = ax.scatter(x_points, y_points, c=collar_var, cmap=cmap, vmin=vlim[0], vmax=vlim[1], s=s, marker=collar_marker, **kwargs) else: plot = ax.scatter(x_points, y_points, c=pointvar, vmin=vlim[0], vmax=vlim[1], s=s, marker=collar_marker, **kwargs) points = np.array([group[figx], group[figy]]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) if var in data.columns: lc = LineCollection(segments, cmap=cmap, norm=plt.Normalize(vlim[0], vlim[1])) lc.set_array(group[var]) else: lc = LineCollection(segments, color=var, norm=plt.Normalize(vlim[0], vlim[1])) lc.set_lw(lw) ax.add_collection(lc) # Plot labels ax = _spatial_labels(ax, orient, griddef, slice_number, title, xlabel, ylabel, unit, sigfigs) if xlim is None: if griddef: if orient in ['xy', 'xz']: xlim = griddef.extents()[0] else: xlim = griddef.extents()[1] else: pad = (data[figx].max() - data[figx].min()) * 0.025 xlim = (data[figx].min() - pad, data[figx].max() + pad) if ylim is None: if griddef: if orient == 'xy': ylim = griddef.extents()[1] else: ylim = griddef.extents()[2] else: pad = (data[figy].max() - data[figy].min()) * 0.025 ylim = (data[figy].min() - pad, data[figy].max() + pad) if aspect is None: aspect = _spatial_aspect(xlim, ylim) if aspect: ax.set_aspect(aspect) # Plot the figure # Note on Tick Labels: # If a group of subplots are put together which share a x-axis, the rotation may not work. By # getting the tick labels generated by matplotlib as a set of label objects, they can be # looped through and have their settings individually fixed. This appears to be the only way # to have the shared axis labels formated properly. The labels are also adjusted closer to # the axis for esthetics. --Warren E. Black # The plots tick labels will not be properly accessible until the figure is "drawn", once the # command below is run, ax.get_ticklabel() will actually work properly. plt.draw() # Check to see if the ytick labels need to be rotated if the rotate argument was not passed. # This doesn't work if the figure is being placed into a subplot (i.e., is not a standalone # figure). Don't know why... ax = _format_tick_labels(ax, rotateticks) # Plot colorbar, the tick locations and labels were generated above using get_vlimdata if cbar: # Set-up axis to place colorbar into # cax = grid.cbar_axes[0] # Plot the colorbar if drill_plot_format: cbar = plt.colorbar(lc, cax=cax, ticks=ticklocs) else: cbar = fig.colorbar(plot, cax=cax, ticks=ticklocs) # Configure the color bar cbar.ax.set_yticklabels(ticklabels, ha='left') cbar.ax.tick_params(axis='y', pad=2) if cbar_label is not None: cbar.set_label(cbar_label, ha='center', va='top', labelpad=2) if axis_xy is None: axis_xy = Parameters['plotting.axis_xy_spatial'] format_plot(ax, grid=grid, axis_xy=axis_xy, xlim=xlim, ylim=ylim) # Export figure if output_file or ('pdfpages' in out_kws): export_image(output_file, **out_kws) # Return whats needed if return_cbar: return ax, cbar if return_plot: return plot return ax