Source code for pygeostat.plotting.location_plot

#!/usr/bin/env python

# -*- coding: utf-8 -*-
"""Location map plotting routine using matplotlib"""
#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
from . set_style import set_plot_style
from .. pygeostat_parameters import Parameters

[docs] @set_plot_style 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: if pointvar is not None and len(pointvar) > 0: plot = ax.scatter(pointx, pointy, c=pointvar, cmap=cmap, vmin=vlim[0], vmax=vlim[1], s=s, marker=marker, **kwargs) else: plot = ax.scatter(pointx, pointy) 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.colormaps.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 = lc.axes.figure.colorbar(lc, cax=cax, ticks=ticklocs) else: cbar = plot.figure.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