#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""plotting a 2D gridded dataset or a slice of a 3D gridded dataset with the ability to include another layer to show point data"""
#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
from . set_style import set_plot_style
import numpy as np
import matplotlib as mpl
[docs]
@set_plot_style
def slice_plot(data, griddef=None, var=None, catdata=None, pointdata=None, pointvar=None,
pointtol=None, pointkws=None, pointcmap=None, orient='xy', slice_number=0, ax=None,
figsize=None, vlim=None, clim=None, title=None, xlabel=None, ylabel=None, unit=None,
rotateticks=None, cbar=True, cbar_label=None, catdict=None, cbar_label_pad=None,
cax=None, sigfigs=3, cmap=None, interp='none', aspect=None, grid=None, axis_xy=None,
rasterize=False, plot_style=None, custom_style=None, output_file=None, out_kws=None,
return_cbar=False, return_plot=False, slice_thickness=None, cbar_nticks=5,
plotformat_dict=None, **kwargs):
"""
slice_plot displays a 2D gridded dataset or a slice of a 3D gridded dataset. To plot only
scattered data, please see :func:`gs.location_plot()<pygeostat.plotting.location_plot>`
The only required parameters are ``data`` and ``griddef``. All other parameters are optional or
calculated automatically. Axis tick labels are automatically checked for overlap and if needed,
are rotated. The figure instance is always returned. To allow for easier modification, the
colorbar object and the data used to generate the plot can also be returned. Examples of their
use are provided bellow.
The values use 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.
Categorical data can be used, however ``catdata`` will need to be set to ``True`` for proper
implementation. Categorical colour palettes are available within pygeostat. See the
documentation for :func:`gs.get_palette() <pygeostat.plotting.utils.get_palette>` for more
information.
Please review the documentation of the :func:`gs.set_style()
<pygeostat.PlotStyle.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: A numpy ndarray, pandas DataFrame or pygeostat DataFile, where each column is a
variable and each row is an observation
griddef (GridDef): A pygeostat GridDef class, which must be provided if a
DataFile is not passed as data with a valid internal GridDef
:class:`gs.GridDef <pygeostat.data.grid_definition.GridDef>`
var (str,int): The name of the column within data to plot. If an int is provided, then it
corresponds with the column number in data. If None, the first column of data
is used.
catdata (bool): Indicate if the data is categorical or not. Will be automatically set if
less than Parameters['plotting.assumecat'] unique values are found within ``data``
pointdata (DataFile): A pygeostat DataFile class created using
:class:`gs.DataFile <pygeostat.data.data.DataFile>` containg point data to overlay the
gridded data with. Must have the necessary coordinate column headers stored within the
class.
pointvar (str): Column header of variable to plot within ``pointdata`` or a permissible
matplotlib colour
pointtol (float): Slice tolerance to plot point data (i.e. plot +/- ``pointtol`` from the
center of the slice). Any negative value plots all data. Default is to plot all data.
pointkws (dict): Optional dictionary of permissible keyword arguments to pass to
matplotlib's scatter function. Default values is ``{'marker':'o', 's':15}``
orient (str): Orientation to slice data. ``'xy'``, ``'xz'``, ``'yz'`` are the only accepted
values
slice_number (int): Grid cell location along the axis not plotted to take the slice of data to
plot
ax (mpl.axis): Matplotlib axis to plot the figure
figsize (tuple): Figure size (width, height)
vlim (float tuple): Data minimum and maximum values
clim (int tuple) or (list): Categorical data minimum and maximum values, Forces categorical
colorbar to plot the full range of categorical values - even if none show in the plot.
Can be either a tuple of integer values OR a list of integers.
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.
xlabel (str): X-axis label
ylabel (str): Y-axis label
unit (str): Unit to place inside the axis label parentheses
rotateticks (bool tuple): Indicate if the axis tick labels should be rotated (x, y)
cbar (bool): Indicate if a colorbar should be plotted or not
cbar_label (str): Colorbar title
catdict (dict): Dictionary to map enumerated catagories to names (e.g., {100: 'Unit1'}).
Taken from Parameters['data.catdict'] if catdata=True and its keys align.
sigfigs (int): Number of sigfigs to consider for the colorbar
cmap (str): Matplotlib or pygeostat colormap or palette
interp (str): plt.imshow interpolation option; ``'spline36'`` (continuous) and
``'hermite'`` (categorical) are good starting points if smoothing is desired.
``'none'`` is the default setting
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'.
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.
rasterize (bool): Indicate if the gridded image should be rasterized during export. The
output resolution is depended on the DPI setting used during export.
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 imshow should be returned. It can be used
to create the colorbars required for subplotting with the ImageGrid()
slice_thickness (int): Number of slices around the selected slice_number to average the attributes
**kwargs: Optional permissible keyword arguments to pass to matplotlib's imshow function
Returns:
Default:
ax (ax): Matplotlib axis instance which contains the gridded figure
return_cbar:
ax (ax), cbar (cbar): Optional, default False. Matplotlib colorbar object
retrun_plot:
ax (ax), plot(?): Optional, default False. Matplotlib colorbar object
return_cbar & return_plot
ax, plot, cbar: default False
**Examples:**
A simple call by passing the data file
.. plot::
import pygeostat as gs
griddef = gs.GridDef([40,0.5,1,40,0.5,1,40,0.5,1])
data_file = gs.ExampleData('3d_grid', griddef)
gs.slice_plot(data_file, orient='xy', cmap='viridis')
|
Plot with slice thickness averaging/upscaling:
.. plot::
import pygeostat as gs
griddef = gs.GridDef([40,0.5,1,40,0.5,1,40,0.5,1])
data_file = gs.ExampleData('3d_grid', griddef)
gs.slice_plot(data_file, orient='xy', cmap='viridis', slice_thickness=20)
.. codeauthor:: pygeostat development team 2016-04-11
"""
import matplotlib.pyplot as plt
from . utils import (_spatial_labels, _spatial_pointdata, _spatial_griddata, _spatial_slice,
_format_tick_labels, format_plot, _spatial_aspect, setup_plot, format_subplot_axis)
from .. pygeostat_parameters import Parameters
from .export_image import export_image
from ..datautils.utils import slice_grid
from ..data.data import DataFile
# Handle dictionary defaults
if out_kws is None:
out_kws = dict()
# Handle var
if var is None:
if isinstance(data, DataFile):
if data.variables is not None:
if isinstance(data.variables, str):
var = data.variables
# Handle pointvar
if pointdata is not None:
if pointvar is None:
if isinstance(var, str):
if var in list(pointdata.columns):
pointvar = var
if pointvar is None:
pointvar = Parameters['plotting.location_plot.c']
# Handle title
if title is None:
if hasattr(data, 'columns'):
if var in data.columns:
title = var
# Parse the data, var and griddef input to determine the data and griddef
data, griddef = _spatial_griddata(data, var, griddef)
# some threshold of uniqueness means its likely categorical?
# also the number of max categories in the palettes
nunique = len(np.unique(data)[np.isfinite(np.unique(data))])
if catdata is None and nunique < Parameters['plotting.assumecat']:
catdata = True
elif catdata is None and isinstance(catdict, dict):
catdata = True
elif catdata is None: # back to original default argument
catdata = False
# Slice the data
if orient in ['xy', 'xz', 'yz']:
view = slice_grid(data, griddef, orient, slice_number, slice_thickness=slice_thickness)
else:
raise ValueError("Not a valid orientation! {}".format(orient))
# Validate then slice the point data if required
try:
if pointtol < 0:
pointtol = None
except:
pass
if pointdata is not None:
# Convert the pointdata into the data frame, x, y and z
pointdata, pointx, pointy, pointz, _ = _spatial_pointdata(pointdata, orient)
# Slice the data
pointx, pointy, pointvari = _spatial_slice(pointdata, pointvar, pointx, pointy,
pointz, griddef, orient, slice_number, pointtol)
if pointx is not None:
if pointvar in pointdata.columns:
if pointcmap is None:
pointcmap = True
else:
pointcmap = None
else:
pointvari = None
pointcmap = None
# Handel grid extent
a = orient[0]
b = orient[1]
xmin = getattr(griddef, a + 'limits')[0]
xmax = getattr(griddef, a + 'limits')[1]
ymin = getattr(griddef, b + 'limits')[0]
ymax = getattr(griddef, b + 'limits')[1]
if aspect is None:
aspect = _spatial_aspect([xmin, xmax], [ymin, ymax])
# handle parms for colorbars and colormaps
output = _color_handling_gridded(data, cmap=cmap, catdata=catdata,
pointcmap=pointcmap, pointvar=pointvari,
view=view, sigfigs=sigfigs, vlim=vlim, clim=clim,
nticks=cbar_nticks, catdict=catdict)
ticklabels, vlim, ticklocs, ncat, pointvari, cmap = output
# Make sure the chosen colormap is set to the point data if required
if pointcmap is True or pointcmap is None:
pointcmap = cmap
# Handel categorical data
if catdata:
tview = view.copy()
for i in range(ncat):
tview[view == ticklabels[i]] = i
if catdict is None:
catdict = Parameters['data.catdict']
if isinstance(catdict, dict):
ticklabels = [catdict[cat] for cat in ticklabels]
else:
tview = view.copy()
# Set-up plot if no axis is supplied using the ImageGrid method if required or the regular way
fig, ax, cax = setup_plot(ax, cbar=cbar, cax=cax, figsize=figsize)
# Handle interpolation in aspect has changed
if aspect != 'equal' and interp == 'none':
if catdata:
interp = 'hermite'
else:
interp = 'spline36'
# Plot gridded data
plot = ax.imshow(tview, origin='lower', extent=[xmin, xmax, ymin, ymax], aspect=aspect,
interpolation=interp, vmin=vlim[0], vmax=vlim[1], cmap=cmap, **kwargs)
# Rasterize the gridded plot
if rasterize:
plot.set_rasterized(True)
# Plot scattered data if required
if pointvari is not None:
# Handel default dictionary
if pointkws is None:
pointkws = {'marker': 'o', 's': Parameters['plotting.location_plot.s']}
if 'marker' not in pointkws:
pointkws['marker'] = 'o'
if 's' not in pointkws:
pointkws['s'] = Parameters['plotting.location_plot.s']
# if 'edgecolors' not in pointkws:
# pointkws['edgecolors'] = 'k'
ax.scatter(pointx, pointy, c=pointvari, cmap=pointcmap,
vmin=vlim[0], vmax=vlim[1], **pointkws)
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
# Plot labels
ax = _spatial_labels(ax, orient, griddef, slice_number=slice_number, title=title, xlabel=xlabel,
ylabel=ylabel, unit=unit)
# 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...
_format_tick_labels(ax, rotateticks=rotateticks)
# Note on Colorbar:
# It is my opinion that colorbars are more esthetically pleasing if they extend the full
# height of the figure it represents. The method that shifts the plot to the left distorts
# its aspect. A divider is used to accomplish the desired effect without distorting the
# figure. It is also important that the tick labels indicate that - in the case of a
# continuous variable - the min and max include their respective greater/less than or equal
# to symbols if they are not the absolute max/min values. --Warren E. Black
# Plot colorbar, the tick locations and labels were generated above using gs.get_vlimdata
if cbar:
# Plot the colorbar
cbar = fig.colorbar(plot, cax=cax, ticks=ticklocs)
# Configure the color bar
if cbar_label_pad is None:
cbar_label_pad = 2
for lbl in ticklabels:
if isinstance(lbl, str):
if 'geq' in lbl or 'leq' in lbl:
cbar_label_pad = -6 + sigfigs
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=cbar_label_pad)
# Format plot based on plotformat_dict if it's available
if plotformat_dict and cbar:
fig, ax, cbar = format_subplot_axis(fig, ax, cbar=cbar,
plotformat_dict=plotformat_dict)
elif plotformat_dict:
fig, ax = format_subplot_axis(fig, ax, cbar=None,
plotformat_dict=plotformat_dict)
if axis_xy is None:
axis_xy = Parameters['plotting.axis_xy_spatial']
format_plot(ax, grid=grid, axis_xy=axis_xy)
# Export figure
if output_file or ('pdfpages' in out_kws and out_kws['pdfpages']):
export_image(output_file, **out_kws)
# Return whats needed
if return_cbar:
if return_plot:
return ax, plot, cbar
else:
return ax, cbar
elif return_plot:
return ax, plot
else:
return ax
def _color_handling_gridded(data, cmap=None, catdata=None, pointcmap=None,
pointvar=None, view=None, sigfigs=3, vlim=None,
clim=None, ticklabels=None, ticklocs=None, nticks=5,
catdict=None):
'''A small utility function called from many of the plotting functions. This sets the colorbar
and colormap parameters and loads the specified colormap.
Parameters:
data: Tidy (long-form) dataframe where each column is a variable and each row is an
observation. Pandas dataframe or numpy array
cmap (str): Matplotlib or pygeostat colormap or palette
catdata (bool): Indicate if the data is categorical or not. Will be automatically set if
less than Parameters['plotting.assumecat'] unique values are found within ``data``
pointcmap (bool): True to color the points using cmap
pointvar: a pandas series of the point data used for the colormap. If categorical data is
passed this will get converted to sequential numbers used in the colormap object.
view: A slice of the data using ``gs.slice_grid()``
sigfigs (int): Number of sigfigs to consider for the colorbar
vlim (float tuple): Data minimum and maximum values
clim (int tuple) or (list): Categorical data minimum and maximum values, Forces categorical
colorbar to plot the full range of categorical values - even if none show in the plot.
Can be either a tuple of integer values OR a list of integers.
ticklabels: Filler. Otherwise you seem to have to set this variable before calling the
function otherwise it gives you an error about assigning a value before the variable is
defined
ticklocs: Filler. Otherwise you seem to have to set this variable before calling the
function otherwise it gives you an error about assigning a value before the variable is
defined
nticks: the number of ticks on the colorbar
Returns:
ticklabels: Matplotlib ticklabels
vlim: If catdata is passed then this is reset to match the colormap. None otherwise
ticklocs: Matplotlib ticklocations
ncat: if categorical data is found then this is the number of categories. None otherwise
pointvar: If categorical data is found then the point view is mapped to the colormap values
cmap: a matplotlib colormap object
'''
from . utils import _get_cmap, get_contcbarargs
if catdata:
if clim:
if isinstance(clim, tuple):
ticklabels = np.arange(clim[0], (clim[1] + 1))
elif isinstance(clim, list):
ticklabels = [int(x) for x in clim]
else:
raise TypeError('Invalid clim type. Must be tuple or list')
else:
ticklabels = np.unique(data[~np.isnan(data.astype(float))])
ticklabels = [int(x) for x in ticklabels]
ncat = len(ticklabels)
if isinstance(catdict, dict) and ncat != len(catdict):
# modify the cmap to store the colors as if cmap is generated from all cats
cmap = _get_cmap(cmap, catdata, len(catdict))
cmap = mpl.colors.ListedColormap(
[clr for cat, clr in zip(catdict, cmap.colors) if cat in ticklabels]
)
else:
cmap = _get_cmap(cmap, catdata, ncat)
vlim = (0, ncat)
ticklocs = np.arange(ncat) + 0.5
if pointcmap is True:
dump = pointvar.copy()
for i in range(ncat):
dump[pointvar == ticklabels[i]] = i
pointvar = dump
else:
if (pointvar is not None) and (pointcmap is not None):
try:
temp_pointdat = pointvar.values()
except:
temp_pointdat = pointvar
temp = np.append(temp_pointdat, view)
vlim, ticklocs, ticklabels = get_contcbarargs(temp, sigfigs, vlim,
nticks=nticks)
else:
vlim, ticklocs, ticklabels = get_contcbarargs(view, sigfigs, vlim,
nticks=nticks)
ncat = None
cmap = _get_cmap(cmap, catdata, ncat)
return ticklabels, vlim, ticklocs, ncat, pointvar, cmap