Source code for spharpy.plot.spatial

"""
Plot functions for spatial data.
"""
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.tri as mtri
import numpy as np
import scipy.spatial as sspat
import pyfar as pf
from matplotlib import colors
from mpl_toolkits.mplot3d import Axes3D
__all__ = [Axes3D]
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from packaging import version
from scipy.stats import circmean

from .cmap import phase_twilight

from pyfar.plot._utils import _add_colorbar
from pyfar.plot.utils import context as pyfar_plot_context
from spharpy.samplings import spherical_voronoi
from spharpy.plot._utils import _prepare_plot
from pyfar.classes.coordinates import sph2cart


def set_aspect_equal_3d(ax):
    """Fix equal aspect bug for 3D plots."""

    xlim = ax.get_xlim3d()
    ylim = ax.get_ylim3d()
    zlim = ax.get_zlim3d()

    from numpy import mean
    xmean = mean(xlim)
    ymean = mean(ylim)
    zmean = mean(zlim)

    plot_radius = max([abs(lim - mean_)
                       for lims, mean_ in ((xlim, xmean),
                                           (ylim, ymean),
                                           (zlim, zmean))
                       for lim in lims])

    ax.set_xlim3d([xmean - plot_radius, xmean + plot_radius])
    ax.set_ylim3d([ymean - plot_radius, ymean + plot_radius])
    ax.set_zlim3d([zmean - plot_radius, zmean + plot_radius])


[docs] def scatter(coordinates, ax=None, style='light', **kwargs): """Plot the x, y, and z coordinates of the sampling grid in the 3d space. Parameters ---------- coordinates : pyfar.Coordinates, spharpy.SamplingSphere The coordinates to be plotted ax : matplotlib.axis, None, optional The matplotlib axis object used for plotting. By default ``None``, which will create a new axis object. style : str ``light`` or ``dark`` to use the pyfar plot styles (see :py:func:`pyfar.plot.context`) or a plot style from :py:data:`matplotlib.style.available`. Pass a dictionary to set specific plot parameters, for example ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary ``style = {}`` to use the currently active plotstyle. The default is ``light``. **kwargs : optional Additional keyword arguments passed to the scatter function. Returns ------- ax : matplotlib.axis The axis object used for plotting. Examples -------- .. plot:: >>> import spharpy >>> coords = spharpy.samplings.gaussian(n_max=5) >>> spharpy.plot.scatter(coords) """ if not isinstance(coordinates, pf.Coordinates): raise ValueError("coordinates must be a coordinates object.") with pyfar_plot_context(style): fig = plt.gcf() if ax is None: ax = plt.gca() if fig.axes else plt.axes(projection='3d') if '3d' not in ax.name: raise ValueError("The projection of the axis needs to be '3d'") ax.scatter(coordinates.x, coordinates.y, coordinates.z, **kwargs) ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') ax.set_box_aspect([ np.ptp(coordinates.x), np.ptp(coordinates.y), np.ptp(coordinates.z)]) return ax
def _triangulation_sphere(sampling, data): """Triangulation for data points sampled on a spherical surface. Parameters ---------- sampling : pyfar.Coordinates, spharpy.SamplingSphere Coordinate object for which the triangulation is calculated data : list of arrays x, y, and z values of the data points in the triangulation Returns ------- triangulation : matplotlib Triangulation """ x, y, z = sph2cart( sampling.azimuth, sampling.colatitude, np.abs(data), ) hull = sspat.ConvexHull( np.asarray(sph2cart( sampling.azimuth, sampling.colatitude, np.ones(len(sampling.colatitude)))).T) tri = mtri.Triangulation(x, y, triangles=hull.simplices) return tri, [x, y, z] def interpolate_data_on_sphere( sampling, data, overlap=np.pi*0.25, refine=False, interpolator='linear'): """Linear interpolator for data on a spherical surface. The interpolator exploits that the data on the sphere is periodic with regard to the elevation and azimuth angle. The data is periodically extended to a specified overlap angle before interpolation. Parameters ---------- sampling : pyfar.Coordinates, spharpy.SamplingSphere The coordinates at which the data is sampled. data : ndarray, double The sampled data points. overlap : float, (pi/4) The overlap for the periodic extension in azimuth angle, given in radians refine : bool Refine the mesh before interpolating. The default is ``False``. interpolator : 'linear', 'cubic' The interpolation method to be used. The default is 'linear'. Returns ------- interp : LinearTriInterpolator, CubicTriInterpolator The interpolator object. Note ---- Internally, matplotlibs LinearTriInterpolator or CubicTriInterpolator are used. """ _, lats, lons = coordinates2latlon(sampling) mask = lons > np.pi - overlap lons = np.concatenate((lons, lons[mask] - np.pi*2)) lats = np.concatenate((lats, lats[mask])) data = np.concatenate((data, data[mask])) mask = lons < -np.pi + overlap lons = np.concatenate((lons, lons[mask] + np.pi*2)) lats = np.concatenate((lats, lats[mask])) data = np.concatenate((data, data[mask])) tri = mtri.Triangulation(lons, lats) if refine: refiner = mtri.UniformTriRefiner(tri) tri, data = refiner.refine_field( data, triinterpolator=mtri.LinearTriInterpolator(tri, data), subdiv=3) if interpolator == 'linear': interpolator = mtri.LinearTriInterpolator(tri, data) elif interpolator == 'cubic': interpolator = mtri.CubicTriInterpolator(tri, data, kind='mind_E') else: raise ValueError("Please give a valid interpolation method.") return interpolator def _balloon_color_data(tri, data, itype): """Return the data array that is to be mapped to the colormap of the balloon. Parameters ---------- tri : Triangulation The matplotlib triangulation for the sphere data : ndarray, double, complex double The data array itype : 'magnitude', 'phase', 'amplitude' What type of data should be extracted. Either the 'magnitude', 'phase', or 'amplitude' of the data array is used for the colormap. Returns ------- color_data : ndarray, double The data array for the colormap. vmin : double The minimum of the color data vmax : double The maximum of the color data """ if itype == 'phase': cdata = np.mod(np.angle(data), 2*np.pi) vmin = 0 vmax = 2*np.pi colors = circmean(cdata[tri.triangles], axis=1) elif itype == 'magnitude': cdata = np.abs(data) vmin = np.min(cdata) vmax = np.max(cdata) colors = np.mean(cdata[tri.triangles], axis=1) elif itype == 'amplitude': vmin = np.min(data) vmax = np.max(data) colors = np.mean(data[tri.triangles], axis=1) else: raise ValueError("Invalid type of data mapping.") return colors, vmin, vmax
[docs] def pcolor_sphere( coordinates, data, cmap=None, colorbar=True, limits=None, cmap_encoding='phase', ax=None, style='light', **kwargs): """Plot data on the surface of a sphere defined by the coordinate angles theta and phi. The data array will be mapped onto the surface of a sphere. Parameters ---------- coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. cmap : str, :py:class:`matplotlib.colors.Colormap`, optional Colormap used for the plot. If ``None`` (default), the colormap is automatically selected based on the value of `cmap_encoding`: ``'phase'`` uses :py:func:`spharpy.plot.phase_twilight`, and ``'magnitude'`` uses the ``'viridis'`` colormap. colorbar : bool, optional Whether to show a colorbar or not. Default is ``True``. limits : tuple, list, optional Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. If ``None``, the limits are set to the minimum and maximum of the data. Default is ``None``. cmap_encoding : str, optional The information encoded in the colormap. Can be either ``'phase'`` (in radians) or ``'magnitude'``. The default is ``'phase'``. ax : matplotlib.axes.Axes or list, tuple or ndarray of matplotlib.axes.Axes Axes to plot on. ``None`` Use the current axis, or create a new axis (and figure) if there is none. ``ax`` If a single axis is passed, this is used for plotting. If `colorbar` is ``True`` the space for the colorbar is taken from this axis. The projection must be ``'3d'``. ``[ax, ax]`` If a list, tuple or array of two axes is passed, the first is used to plot the data and the second to plot the colorbar. In this case `colorbar` must be ``True`` and the projection of the second axis must be ``'rectilinear'``. The first axis must meet the same conditions as for the case when a single axis is passed. The default is ``None``. style : str ``light`` or ``dark`` to use the pyfar plot styles (see :py:func:`pyfar.plot.context`) or a plot style from :py:data:`matplotlib.style.available`. Pass a dictionary to set specific plot parameters, for example ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary ``style = {}`` to use the currently active plotstyle. The default is ``light``. **kwargs : optional Additional arguments passed to the plot_trisurf function. Returns ------- ax : matplotlib.axes.Axes, list[matplotlib.axes.Axes] If `colorbar` is ``True`` a list of two axes is returned. The first one is the axis on which the data is plotted, the second one is the axis of the colorbar. If `colorbar` is ``False``, only the axis on which the data is plotted is returned. plot : matplotlib.trisurf The trisurf object created by the function. cb : matplotlib.colorbar.Colorbar, None The Matplotlib colorbar object if `colorbar` is ``True`` and ``None`` otherwise. This can be used to control the appearance of the colorbar, e.g., the label can be set by ``colorbar.set_label()``. Examples -------- .. plot:: >>> import spharpy >>> import numpy as np >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) >>> data = np.sin(coords.colatitude) * np.cos(coords.azimuth) >>> spharpy.plot.pcolor_sphere(coords, data, cmap_encoding='phase') """ _check_input_parameters(coordinates, data, cmap, colorbar, limits, ax) if cmap_encoding not in ['phase', 'magnitude']: raise ValueError( "cmap_encoding must be either 'phase' or 'magnitude'.") tri, xyz = _triangulation_sphere(coordinates, np.ones_like(data)) with pyfar_plot_context(style): fig, ax = _prepare_plot(ax, '3d') if not isinstance(ax, (list, tuple, np.ndarray)): ax = [ax, None] if '3d' not in ax[0].name: raise ValueError("The projection of the axis needs to be '3d'") if cmap_encoding == 'phase': if cmap is None: cmap = phase_twilight() clabel = 'Phase (rad)' elif cmap_encoding == 'magnitude': if cmap is None: cmap = plt.get_cmap('viridis') clabel = 'Magnitude' cdata, vmin, vmax = _balloon_color_data(tri, data, cmap_encoding) if limits is not None: vmin, vmax = limits plot = ax[0].plot_trisurf(tri, xyz[2], cmap=cmap, antialiased=True, vmin=vmin, vmax=vmax, **kwargs) plot.set_array(cdata) cb = _add_colorbar(colorbar, fig, ax, plot, clabel) # reduce to plot-axis, colorbar-axis will be returned as cb.ax ax = ax[0] ax.set_xlabel('x[m]') ax.set_ylabel('y[m]') ax.set_zlabel('z[m]') ax.set_box_aspect([ np.ptp(coordinates.x), np.ptp(coordinates.y), np.ptp(coordinates.z)]) if colorbar: ax = [ax, cb.ax] return (ax, plot, cb)
[docs] def balloon_wireframe( coordinates, data, cmap=None, colorbar=True, limits=None, cmap_encoding='phase', ax=None, style='light', **kwargs): """Plot data on a sphere defined by the coordinate angles theta and phi. The magnitude information is mapped onto the radius of the sphere. The colormap represents either the phase or the magnitude of the data array. Parameters ---------- coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. cmap : str, :py:class:`matplotlib.colors.Colormap`, optional Colormap used for the plot. If ``None`` (default), the colormap is automatically selected based on the value of `cmap_encoding`: ``'phase'`` uses :py:func:`spharpy.plot.phase_twilight`, and ``'magnitude'`` uses the ``'viridis'`` colormap. colorbar : bool, optional Whether to show a colorbar or not. Default is ``True``. limits : tuple, list, optional Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. If ``None``, the limits are set to the minimum and maximum of the data. Default is ``None``. cmap_encoding : str, optional The information encoded in the colormap. Can be either ``'phase'`` (in radians) or ``'magnitude'``. The default is ``'phase'``. ax : matplotlib.axes.Axes or list, tuple or ndarray of matplotlib.axes.Axes Axes to plot on. ``None`` Use the current axis, or create a new axis (and figure) if there is none. ``ax`` If a single axis is passed, this is used for plotting. If `colorbar` is ``True`` the space for the colorbar is taken from this axis. The projection must be ``'3d'``. ``[ax, ax]`` If a list, tuple or array of two axes is passed, the first is used to plot the data and the second to plot the colorbar. In this case `colorbar` must be ``True`` and the projection of the second axis must be ``'rectilinear'``. The first axis must meet the same conditions as for the case when a single axis is passed. The default is ``None``. style : str ``light`` or ``dark`` to use the pyfar plot styles (see :py:func:`pyfar.plot.context`) or a plot style from :py:data:`matplotlib.style.available`. Pass a dictionary to set specific plot parameters, for example ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary ``style = {}`` to use the currently active plotstyle. The default is ``light``. **kwargs : optional Additional arguments passed to the plot_trisurf function. Returns ------- ax : matplotlib.axes.Axes, list[matplotlib.axes.Axes] If `colorbar` is ``True`` a list of two axes is returned. The first one is the axis on which the data is plotted, the second one is the axis of the colorbar. If `colorbar` is ``False``, only the axis on which the data is plotted is returned. plot : matplotlib.trisurf The trisurf object created by the function. cb : matplotlib.colorbar.Colorbar, None The Matplotlib colorbar object if `colorbar` is ``True`` and ``None`` otherwise. This can be used to control the appearance of the colorbar, e.g., the label can be set by ``colorbar.set_label()``. Examples -------- .. plot:: >>> import spharpy >>> import numpy as np >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) >>> data = np.sin(coords.azimuth) * np.cos(coords.elevation) >>> spharpy.plot.balloon_wireframe(coords, data, cmap_encoding='phase') """ # input checks _check_input_parameters(coordinates, data, cmap, colorbar, limits, ax) if cmap_encoding not in ['phase', 'magnitude']: raise ValueError( "cmap_encoding must be either 'phase' or 'magnitude'.") if isinstance(cmap, str): cmap = plt.get_cmap(cmap) tri, xyz = _triangulation_sphere(coordinates, data) with pyfar_plot_context(style): fig, ax = _prepare_plot(ax, '3d') if not isinstance(ax, (list, tuple, np.ndarray)): ax = [ax, None] if '3d' not in ax[0].name: raise ValueError("The projection of the axis needs to be '3d'") if cmap_encoding == 'phase': if cmap is None: cmap = phase_twilight() clabel = 'Phase (rad)' elif cmap_encoding == 'magnitude': if cmap is None: cmap = plt.get_cmap('viridis') clabel = 'Magnitude' cdata, vmin, vmax = _balloon_color_data(tri, data, cmap_encoding) if limits is not None: vmin, vmax = limits plot = ax[0].plot_trisurf(tri, xyz[2], cmap=cmap, antialiased=True, vmin=vmin, vmax=vmax, **kwargs) # Set values to `None`. Otherwise theses values will always overwrite # what is set using plot.set_facecolors. plot.set_array(None) cnorm = plt.Normalize(vmin, vmax) # Get colors from cdata for plot.set_edgecolors cmap_colors = cmap(cnorm(cdata)) cmappable = mpl.cm.ScalarMappable(cnorm, cmap) cmappable.set_array(np.linspace(vmin, vmax, cdata.size)) plot.set_edgecolors(cmap_colors) plot.set_facecolors(np.ones(cmap_colors.shape)*0.9) cb = _add_colorbar(colorbar, fig, ax, plot, clabel) # reduce to plot-axis, colorbar-axis will be returned as cb.ax ax = ax[0] ax.set_xlabel('x[m]') ax.set_ylabel('y[m]') ax.set_zlabel('z[m]') ax.set_box_aspect([ np.ptp(xyz[0]), np.ptp(xyz[1]), np.ptp(xyz[2])]) if colorbar: ax = [ax, cb.ax] return (ax, plot, cb)
[docs] def balloon( coordinates, data, cmap=None, colorbar=True, limits=None, cmap_encoding='phase', ax=None, style='light', **kwargs): """Plot data on a sphere defined by the coordinate angles theta and phi. The magnitude information is mapped onto the radius of the sphere. The colormap represents either the phase or the magnitude of the data array. Parameters ---------- coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. cmap : str, :py:class:`matplotlib.colors.Colormap`, optional Colormap used for the plot. If ``None`` (default), the colormap is automatically selected based on the value of `cmap_encoding`: ``'phase'`` uses :py:func:`spharpy.plot.phase_twilight`, and ``'magnitude'`` uses the ``'viridis'`` colormap. colorbar : bool, optional Whether to show a colorbar or not. Default is ``True``. limits : tuple, list, optional Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. If ``None``, the limits are set to the minimum and maximum of the data. Default is ``None``. cmap_encoding : str, optional The information encoded in the colormap. Can be either ``'phase'`` (in radians) or ``'magnitude'``. The default is ``'phase'``. ax : matplotlib.axes.Axes or list, tuple or ndarray of matplotlib.axes.Axes Axes to plot on. ``None`` Use the current axis, or create a new axis (and figure) if there is none. ``ax`` If a single axis is passed, this is used for plotting. If `colorbar` is ``True`` the space for the colorbar is taken from this axis. The projection must be ``'3d'``. ``[ax, ax]`` If a list, tuple or array of two axes is passed, the first is used to plot the data and the second to plot the colorbar. In this case `colorbar` must be ``True`` and the projection of the second axis must be ``'rectilinear'``. The first axis must meet the same conditions as for the case when a single axis is passed. The default is ``None``. style : str ``light`` or ``dark`` to use the pyfar plot styles (see :py:func:`pyfar.plot.context`) or a plot style from :py:data:`matplotlib.style.available`. Pass a dictionary to set specific plot parameters, for example ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary ``style = {}`` to use the currently active plotstyle. The default is ``light``. **kwargs : optional Additional arguments passed to the plot_trisurf function. Returns ------- ax : matplotlib.axes.Axes, list[matplotlib.axes.Axes] If `colorbar` is ``True`` a list of two axes is returned. The first one is the axis on which the data is plotted, the second one is the axis of the colorbar. If `colorbar` is ``False``, only the axis on which the data is plotted is returned. plot : matplotlib.trisurf The trisurf object created by the function. cb : matplotlib.colorbar.Colorbar, None The Matplotlib colorbar object if `colorbar` is ``True`` and ``None`` otherwise. This can be used to control the appearance of the colorbar, e.g., the label can be set by ``colorbar.set_label()``. Examples -------- .. plot:: >>> import spharpy >>> import numpy as np >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) >>> data = np.sin(coords.azimuth) * np.cos(coords.elevation) >>> spharpy.plot.balloon(coords, data, cmap_encoding='phase') """ # input checks _check_input_parameters(coordinates, data, cmap, colorbar, limits, ax) if cmap_encoding not in ['phase', 'magnitude']: raise ValueError( "cmap_encoding must be either 'phase' or 'magnitude'.") tri, xyz = _triangulation_sphere(coordinates, data) with pyfar_plot_context(style): fig, ax = _prepare_plot(ax, '3d') # _add_colorbar expects a list of axes if not isinstance(ax, (list, tuple, np.ndarray)): ax = [ax, None] if '3d' not in ax[0].name: raise ValueError("The projection of the axis needs to be '3d'") if cmap_encoding == 'phase': if cmap is None: cmap = phase_twilight() clabel = 'Phase (rad)' elif cmap_encoding == 'magnitude': if cmap is None: cmap = plt.get_cmap('viridis') clabel = cmap_encoding.title() cdata, vmin, vmax = _balloon_color_data(tri, data, cmap_encoding) if limits is not None: vmin, vmax = limits plot = ax[0].plot_trisurf(tri, xyz[2], cmap=cmap, antialiased=True, vmin=vmin, vmax=vmax, **kwargs) plot.set_array(cdata) ax[0].set_box_aspect([ np.ptp(xyz[0]), np.ptp(xyz[1]), np.ptp(xyz[2])]) cb = _add_colorbar(colorbar, fig, ax, plot, clabel) # reduce to plot-axis, colorbar-axis will be returned as cb.ax ax = ax[0] ax.set_xlabel('x[m]') ax.set_ylabel('y[m]') ax.set_zlabel('z[m]') if colorbar: ax = [ax, cb.ax] return (ax, plot, cb)
[docs] def voronoi_cells_sphere(sampling, round_decimals=13, ax=None, style='light'): """Plot the Voronoi cells of a Voronoi tesselation on a sphere. Parameters ---------- sampling : pyfar.Coordinates, spharpy.SamplingSphere Sampling as SamplingSphere object round_decimals : int Decimals to be rounded to for eliminating duplicate points in the voronoi diagram. Default is ``13``. ax : AxesSubplot, None, optional The subplot axes to use for plotting. The used projection needs to be ``'3d'``. style : str ``light`` or ``dark`` to use the pyfar plot styles (see :py:func:`pyfar.plot.context`) or a plot style from :py:data:`matplotlib.style.available`. Pass a dictionary to set specific plot parameters, for example ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary ``style = {}`` to use the currently active plotstyle. The default is ``light``. Returns ------- ax : matplotlib.axis The axis object used for plotting. Examples -------- .. plot:: >>> import spharpy >>> coords = spharpy.samplings.gaussian(n_max=5) >>> spharpy.plot.voronoi_cells_sphere(coords) """ if not isinstance(sampling, pf.Coordinates): raise ValueError("sampling must be a coordinates object.") sv = spherical_voronoi(sampling, round_decimals=round_decimals) sv.sort_vertices_of_regions() points = sampling.cartesian.T with pyfar_plot_context(style): fig = plt.gcf() if ax is None: ax = plt.gca() if fig.axes else plt.axes(projection='3d') if '3d' not in ax.name: raise ValueError("The projection of the axis needs to be '3d'") if version.parse(mpl.__version__) < version.parse('3.1.0'): ax.set_aspect('equal') # plot the unit sphere for reference (optional) u = np.linspace(0, 2 * np.pi, 100) v = np.linspace(0, np.pi, 100) x = np.outer(np.cos(u), np.sin(v)) y = np.outer(np.sin(u), np.sin(v)) z = np.outer(np.ones(np.size(u)), np.cos(v)) ax.plot_surface(x, y, z, color='y', alpha=0.1) ax.scatter(points[0], points[1], points[2], c='r') for region in sv.regions: polygon = Poly3DCollection( [sv.vertices[region]], alpha=0.5, facecolor=None) polygon.set_edgecolor((0, 0, 0, 1)) polygon.set_facecolor((1, 1, 1, 0.)) ax.add_collection3d(polygon) ax.set_box_aspect([ np.ptp(sampling.x), np.ptp(sampling.y), np.ptp(sampling.z)]) ax.set_xlabel('x[m]') ax.set_ylabel('y[m]') ax.set_zlabel('z[m]') return ax
def _combined_contour(x, y, data, limits, cmap, levels, ax): """Combine a filled contour plot with a black line contour plot for better highlighting. Parameters ---------- x : ndarray, double The x coordinates. y : ndarray, double The y coordinates. data : ndarray, double The data array. limits : tuple, list Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. cmap : :py:class:`matplotlib.colors.Colormap` Colormap for the plot, see matplotlib.cm. levels : int or array-like Determines the number and positions of the contours. If an int n, use :py:class:`matplotlib.ticker.MaxNLocator`, which tries to automatically choose no more than n+1 contour levels between minimum and maximum numeric values of the plot data. If array-like, draw contour lines at the specified levels. The values must be in increasing order. ax : matplotlib.axes The axes object into which the contour is plotted Returns ------- cf : matplotlib.ScalarMappable The scalar mappable for the contour plot """ extend = 'neither' if limits is None: limits = (data.min(), data.max()) else: mask_min = data < limits[0] data[mask_min] = limits[0] mask_max = data > limits[1] data[mask_max] = limits[1] if np.any(mask_max) & np.any(mask_min): extend = 'both' elif np.any(mask_max) & ~np.any(mask_min): extend = 'max' elif ~np.any(mask_max) & np.any(mask_min): extend = 'min' ax.tricontour(x, y, data, levels=levels, linewidths=0.5, colors='k', vmin=limits[0], vmax=limits[1], extend=extend) return ax.tricontourf( x, y, data, levels=levels, cmap=cmap, vmin=limits[0], vmax=limits[1], extend=extend)
[docs] def pcolor_map( coordinates, data, cmap='viridis', colorbar=True, limits=None, projection='mollweide', refine=False, ax=None, style='light', **kwargs): """ Plot the map projection of data points sampled on a spherical surface. The data has to be real. Notes ----- In case limits are given, all out of bounds data will be clipped to the respective limit. Parameters ---------- coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : numpy.ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. cmap : str, :py:class:`matplotlib.colors.Colormap`, optional Colormap for the plot, see matplotlib.cm. Default is ``'viridis'``. colorbar : bool, optional Whether to show a colorbar or not. Default is ``True``. limits : tuple, list, optional Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. If ``None``, the limits are set to the minimum and maximum of the data. Default is ``None``. projection : str, optional The projection of the map. Default is ``'mollweide'``. See :py:doc:`matplotlib:gallery/subplots_axes_and_figures/geo_demo` for more information on available projections in matplotlib. refine : bool, optional Whether to refine the triangulation before plotting. Default is ``False``. ax : matplotlib.axes.Axes or list, tuple or ndarray of matplotlib.axes.Axes Axes to plot on. ``None`` Use the current axis, or create a new axis (and figure) if there is none. ``ax`` If a single axis is passed, this is used for plotting. If `colorbar` is ``True`` the space for the colorbar is taken from this axis. Projection should be one of the :doc:`matplotlib:gallery/subplots_axes_and_figures/geo_demo`. ``[ax, ax]`` If a list, tuple or array of two axes is passed, the first is used to plot the data and the second to plot the colorbar. In this case `colorbar` must be ``True`` and the projection of the second axis must be ``'rectilinear'``. The first axis must meet the same conditions as for the case when a single axis is passed. The default is ``None``. style : str ``light`` or ``dark`` to use the pyfar plot styles (see :py:func:`pyfar.plot.context`) or a plot style from :py:data:`matplotlib.style.available`. Pass a dictionary to set specific plot parameters, for example ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary ``style = {}`` to use the currently active plotstyle. The default is ``light``. **kwargs : optional Additional arguments passed to the tripcolor function. Returns ------- ax : matplotlib.axes.Axes, list[matplotlib.axes.Axes] If `colorbar` is ``True`` a list of two axes is returned. The first one is the axis on which the data is plotted, the second one is the axis of the colorbar. If `colorbar` is ``False``, only the axis on which the data is plotted is returned. cf : matplotlib.tri.TriContourSet The contour plot object. cb : matplotlib.colorbar.Colorbar, None The Matplotlib colorbar object if `colorbar` is ``True`` and ``None`` otherwise. This can be used to control the appearance of the colorbar, e.g., the label can be set by ``colorbar.set_label()``. Examples -------- .. plot:: >>> import spharpy >>> import numpy as np >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) >>> data = np.sin(2*coords.colatitude) * np.cos(2*coords.azimuth) >>> spharpy.plot.pcolor_map(coords, data) """ # input checks _check_input_parameters(coordinates, data, cmap, colorbar, limits, ax) if not isinstance(refine, bool): raise ValueError("refine must be a boolean.") height, latitude, longitude = coordinates2latlon(coordinates) tri = mtri.Triangulation(longitude, latitude) if refine is not None: subdiv = refine if isinstance(refine, int) else 2 refiner = mtri.UniformTriRefiner(tri) tri, data = refiner.refine_field( data, triinterpolator=mtri.LinearTriInterpolator(tri, data), subdiv=subdiv) with pyfar_plot_context(style): fig, ax = _prepare_plot(ax, projection) if not isinstance(ax, (list, tuple, np.ndarray)): ax = [ax, None] if ax[0].name != projection: raise ValueError( f"The projection of the axis needs to be '{projection}'" f", but is '{ax[0].name}'") ax[0].set_xlabel('Longitude [$^\\circ$]') ax[0].set_ylabel('Latitude [$^\\circ$]') extend = 'neither' if limits is None: limits = (data.min(), data.max()) else: mask_min = data < limits[0] data[mask_min] = limits[0] mask_max = data > limits[1] data[mask_max] = limits[1] if np.any(mask_max) & np.any(mask_min): extend = 'both' elif np.any(mask_max) & ~np.any(mask_min): extend = 'max' elif ~np.any(mask_max) & np.any(mask_min): extend = 'min' cf = ax[0].tripcolor( tri, data, cmap=cmap, vmin=limits[0], vmax=limits[1], **kwargs) plt.grid(True) cb = _add_colorbar(colorbar, fig, ax, cf, 'Amplitude') ax = ax[0] if colorbar: cb.extend = extend ax = [ax, cb.ax] return (ax, cf, cb)
[docs] def contour_map( coordinates, data, cmap='viridis', colorbar=True, limits=None, projection='mollweide', levels=None, ax=None, style='light'): """ Plot the map projection of data points sampled on a spherical surface. The data has to be real. Notes ----- In case limits are given, all out of bounds data will be clipped to the respective limit. Parameters ---------- coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. cmap : str, :py:class:`matplotlib.colors.Colormap`, optional Colormap for the plot, see matplotlib.cm. Default is ``'viridis'``. colorbar : bool, optional Whether to show a colorbar or not. Default is ``True``. limits : tuple, list, optional Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. If ``None``, the limits are set to the minimum and maximum of the data. Default is ``None``. projection : str, optional The projection of the map. Default is ``'mollweide'``. See :py:doc:`matplotlib:gallery/subplots_axes_and_figures/geo_demo` for more information on available projections in matplotlib. levels : int or array-like, optional Determines the number and positions of the contours. If an int n, use :py:class:`matplotlib.ticker.MaxNLocator`, which tries to automatically choose no more than n+1 contour levels between minimum and maximum numeric values of the plot data. If array-like, draw contour lines at the specified levels. The values must be in increasing order. Default is ``None``, the levels are chosen automatically by Matplotlib. ax : matplotlib.axes.Axes or list, tuple or ndarray of matplotlib.axes.Axes Axes to plot on. ``None`` Use the current axis, or create a new axis (and figure) if there is none. ``ax`` If a single axis is passed, this is used for plotting. If `colorbar` is ``True`` the space for the colorbar is taken from this axis. Projection should be one of the :doc:`matplotlib:gallery/subplots_axes_and_figures/geo_demo`. ``[ax, ax]`` If a list, tuple or array of two axes is passed, the first is used to plot the data and the second to plot the colorbar. In this case `colorbar` must be ``True`` and the projection of the second axis must be ``'rectilinear'``. The first axis must meet the same conditions as for the case when a single axis is passed. The default is ``None``. style : str ``light`` or ``dark`` to use the pyfar plot styles (see :py:func:`pyfar.plot.context`) or a plot style from :py:data:`matplotlib.style.available`. Pass a dictionary to set specific plot parameters, for example ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary ``style = {}`` to use the currently active plotstyle. The default is ``light``. Returns ------- ax : matplotlib.axes.Axes, list[matplotlib.axes.Axes] If `colorbar` is ``True`` a list of two axes is returned. The first one is the axis on which the data is plotted, the second one is the axis of the colorbar. If `colorbar` is ``False``, only the axis on which the data is plotted is returned. cf : matplotlib.contour.QuadContourSet The contour plot object. cb : matplotlib.colorbar.Colorbar, None The Matplotlib colorbar object if `colorbar` is ``True`` and ``None`` otherwise. This can be used to control the appearance of the colorbar, e.g., the label can be set by ``colorbar.set_label()``. Examples -------- .. plot:: >>> import spharpy >>> import numpy as np >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) >>> data = np.sin(2*coords.colatitude) * np.cos(2*coords.azimuth) >>> spharpy.plot.contour_map(coords, data) """ # input checks _check_input_parameters(coordinates, data, cmap, colorbar, limits, ax) data = data.copy() with pyfar_plot_context(style): fig, ax = _prepare_plot(ax, projection) if not isinstance(ax, (list, tuple, np.ndarray)): ax = [ax, None] if ax[0].name != projection: raise ValueError( f"The projection of the axis needs to be '{projection}'" f", but is '{ax[0].name}'") ax[0].set_xlabel('Longitude [$^\\circ$]') ax[0].set_ylabel('Latitude [$^\\circ$]') _, latitude, longitude = coordinates2latlon(coordinates) cf = _combined_contour(longitude, latitude, data, limits, cmap, levels, ax[0]) if type(levels) is int: levels = mpl.ticker.MaxNLocator(levels) plt.grid(True) cb = _add_colorbar(colorbar, fig, ax, cf, 'Amplitude') if colorbar and levels is not None: cb.set_ticks(levels) ax = ax[0] if colorbar: ax = [ax, cb.ax] return (ax, cf, cb)
[docs] def contour( coordinates, data, cmap='viridis', colorbar=True, limits=None, levels=None, ax=None, style='light'): """ Plot the map projection of data points sampled on a spherical surface. The data has to be real-valued. Notes ----- In case limits are given, all out of bounds data will be clipped to the respective limit. Parameters ---------- coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data: ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. cmap : str, :py:class:`matplotlib.colors.Colormap`, optional Colormap for the plot, see matplotlib.cm. Default is ``'viridis'``. colorbar : bool, optional Whether to show a colorbar or not. Default is ``True``. limits : tuple, list, optional Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. If ``None``, the limits are set to the minimum and maximum of the data. Default is ``None``. levels : int or array-like, optional Determines the number and positions of the contours. If an int n, use :py:class:`matplotlib.ticker.MaxNLocator`, which tries to automatically choose no more than n+1 contour levels between minimum and maximum numeric values of the plot data. If array-like, draw contour lines at the specified levels. The values must be in increasing order. Default is ``None``, the levels are chosen automatically by Matplotlib. ax : matplotlib.axes.Axes or list, tuple or ndarray of matplotlib.axes.Axes Axes to plot on. ``None`` Use the current axis, or create a new axis (and figure) if there is none. ``ax`` If a single axis is passed, this is used for plotting. If `colorbar` is ``True`` the space for the colorbar is taken from this axis. The projection must be ``'rectilinear'``. ``[ax, ax]`` If a list, tuple or array of two axes is passed, the first is used to plot the data and the second to plot the colorbar. In this case `colorbar` must be ``True`` and the projection of the second axis must be ``'rectilinear'``. The first axis must meet the same conditions as for the case when a single axis is passed. The default is ``None``. style : str ``light`` or ``dark`` to use the pyfar plot styles (see :py:func:`pyfar.plot.context`) or a plot style from :py:data:`matplotlib.style.available`. Pass a dictionary to set specific plot parameters, for example ``style = {'axes.facecolor':'black'}``. Pass an empty dictionary ``style = {}`` to use the currently active plotstyle. The default is ``light``. Returns ------- ax : matplotlib.axes.Axes, list[matplotlib.axes.Axes] If `colorbar` is ``True`` a list of two axes is returned. The first one is the axis on which the data is plotted, the second one is the axis of the colorbar. If `colorbar` is ``False``, only the axis on which the data is plotted is returned. cf : matplotlib.contour.QuadContourSet The contour plot object. cb : matplotlib.colorbar.Colorbar, None The Matplotlib colorbar object if `colorbar` is ``True`` and ``None`` otherwise. This can be used to control the appearance of the colorbar, e.g., the label can be set by ``colorbar.set_label()``. Examples -------- .. plot:: >>> import spharpy >>> import numpy as np >>> coords = spharpy.samplings.equal_area(n_max=0, n_points=500) >>> data = np.sin(2*coords.colatitude) * np.cos(2*coords.azimuth) >>> spharpy.plot.contour(coords, data) """ # input checks _check_input_parameters(coordinates, data, cmap, colorbar, limits, ax) data = data.copy() _, latitude, longitude = coordinates2latlon(coordinates) lat_deg = latitude * 180/np.pi lon_deg = longitude * 180/np.pi with pyfar_plot_context(style): fig, ax = _prepare_plot(ax, 'rectilinear') if not isinstance(ax, (list, tuple, np.ndarray)): ax = [ax, None] if ax[0].name != 'rectilinear': raise ValueError( f"The projection of the axis needs to be 'rectilinear'" f", but is '{ax[0].name}'") ax[0].set_xlabel('Longitude [$^\\circ$]') ax[0].set_ylabel('Latitude [$^\\circ$]') cf = _combined_contour(lon_deg, lat_deg, data, limits, cmap, levels, ax[0]) if type(levels) is int: levels = mpl.ticker.MaxNLocator(levels) plt.grid(True) cb = _add_colorbar(colorbar, fig, ax, cf, 'Amplitude') if colorbar and levels is not None: cb.set_ticks(levels) ax = ax[0] if colorbar: ax = [ax, cb.ax] return (ax, cf, cb)
[docs] class MidpointNormalize(colors.Normalize): """Colormap norm with a defined midpoint. Useful for normalization of colormaps representing deviations from a defined midpoint. Taken from the official matplotlib documentation at https://matplotlib.org/users/colormapnorms.html. """
[docs] def __init__(self, vmin=None, vmax=None, midpoint=0., clip=False): self.midpoint = midpoint colors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None): # noqa: ARG002 # I'm ignoring masked values and all kinds of edge cases to make a # simple example... x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1] return np.ma.masked_array(np.interp(value, x, y))
def coordinates2latlon(coords: pf.Coordinates): r"""Transforms from Cartesian coordinates to Geocentric coordinates. .. math:: h = \sqrt{x^2 + y^2 + z^2}, \theta = \pi/2 - \arccos(\frac{z}{r}), \phi = \arctan(\frac{y}{x}) -\pi/2 < \theta < \pi/2, -\pi < \phi < \pi where :math:`h` is the heigth, :math:`\theta` is the latitude angle and :math:`\phi` is the longitude angle Parameters ---------- coords : pyfar.Coordinates, spharpy.SamplingSphere Cartesian Coordiantes are transformed to Geocentric coordinates Returns ------- height : ndarray, number The radius is rendered as height information latitude : ndarray, number Geocentric latitude angle longitude : ndarray, number Geocentric longitude angle """ x = coords.x y = coords.y z = coords.z height = np.sqrt(x**2 + y**2 + z**2) latitude = np.pi/2 - np.arccos(z/height) longitude = np.arctan2(y, x) return height, latitude, longitude def _check_input_parameters(coordinates, data, cmap, colorbar, limits, ax=None): """Check the input parameters for the plotting functions. The function raises ValueError if the input parameters are not valid. Parameters ---------- coordinates : pyfar.Coordinates, spharpy.SamplingSphere Coordinates defining a sphere data : ndarray, double Data for each angle, must have size corresponding to the number of points given in coordinates. cmap : str, :py:class:`matplotlib.colors.Colormap` Colormap for the plot, see matplotlib.cm. colorbar : bool, optional Whether to show a colorbar or not. Default is `True`. limits : tuple, list, optional Tuple or list containing the maximum and minimum to which the colormap needs to be clipped. If `None`, the limits are set to the minimum and maximum of the data. ax : matplotlib.axes.Axes or list, tuple or ndarray of matplotlib.axes.Axes Axes to plot on. """ if not isinstance(colorbar, bool): raise ValueError("colorbar must be a boolean.") if not isinstance(cmap, (str, type(None), mpl.colors.Colormap)): raise ValueError( "cmap must be a string, Colormap object, or None.") if not isinstance(coordinates, pf.Coordinates): raise ValueError("coordinates must be a coordinates object.") if not isinstance(data, np.ndarray): raise ValueError( "data must be a 1D array with the same cshape as the coordinates.") if data.shape[-1] != coordinates.cshape[-1]: raise ValueError( "data must be a 1D array with the same cshape as the coordinates.") if limits is not None and not isinstance(limits, (tuple, list)): raise ValueError( "limits must be a tuple or list containing the minimum and " "maximum values for the colormap or None.") if limits is not None and len(limits) != 2: raise ValueError( "limits must be a tuple or list containing the minimum and " "maximum values for the colormap or None.") if not colorbar and isinstance(ax, (tuple, list, np.ndarray)): raise ValueError( "A list of axes can not be used if colorbar is False") if not (ax is None or isinstance(ax, (list, tuple, np.ndarray, plt.Axes)))\ or (isinstance(ax, (list, tuple, np.ndarray)) and np.asarray(ax).shape != (2,)): raise ValueError( "ax can be ``None``, a single matplotlib.axes.Axes object or a " "list, tuple or array of two axes", ) if isinstance(ax, (tuple, list, np.ndarray)) \ and (ax[1].name != 'rectilinear'): raise ValueError( "If [ax1, ax2] is passed ax2 needs to be of 'rectilinear' " "projection", )