• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

morganjwilliams / pyrolite / 5564819928

pending completion
5564819928

push

github

morganjwilliams
Merge branch 'release/0.3.3' into main

249 of 270 new or added lines in 48 files covered. (92.22%)

217 existing lines in 33 files now uncovered.

5971 of 6605 relevant lines covered (90.4%)

10.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.94
/pyrolite/plot/density/__init__.py
1
"""
2
Kernel desnity estimation plots for geochemical data.
3
"""
4
import copy
12✔
5

6
import matplotlib.pyplot as plt
12✔
7
import numpy as np
12✔
8
from matplotlib.ticker import MaxNLocator
12✔
9

10
from ...comp.codata import close
12✔
11
from ...util.log import Handle
12✔
12
from ...util.meta import get_additional_params, subkwargs
12✔
13
from ...util.plot.axes import add_colorbar, init_axes
12✔
14
from ...util.plot.density import (
12✔
15
    get_axis_density_methods,
16
    percentile_contour_values_from_meshz,
17
    plot_Z_percentiles,
18
)
19
from ...util.plot.style import DEFAULT_CONT_COLORMAP
12✔
20
from .grid import DensityGrid
12✔
21
from .ternary import ternary_heatmap
12✔
22

23
logger = Handle(__name__)
12✔
24

25

26
def density(
12✔
27
    arr,
28
    ax=None,
29
    logx=False,
30
    logy=False,
31
    bins=25,
32
    mode="density",
33
    extent=None,
34
    contours=[],
35
    percentiles=True,
36
    relim=True,
37
    cmap=DEFAULT_CONT_COLORMAP,
38
    shading="auto",
39
    vmin=0.0,
40
    colorbar=False,
41
    **kwargs
42
):
43
    """
44
    Creates diagramatic representation of data density and/or frequency for either
45
    binary diagrams (X-Y) or ternary plots.
46
    Additional arguments are typically forwarded
47
    to respective :mod:`matplotlib` functions
48
    :func:`~matplotlib.pyplot.pcolormesh`,
49
    :func:`~matplotlib.pyplot.hist2d`,
50
    :func:`~matplotlib.pyplot.hexbin`,
51
    :func:`~matplotlib.pyplot.contour`, and
52
    :func:`~matplotlib.pyplot.contourf` (see Other Parameters, below).
53

54
    Parameters
55
    ----------
56
    arr : :class:`numpy.ndarray`
57
        Dataframe from which to draw data.
58
    ax : :class:`matplotlib.axes.Axes`, `None`
59
        The subplot to draw on.
60
    logx : :class:`bool`, `False`
61
        Whether to use a logspaced *grid* on the x axis. Values strictly >0 required.
62
    logy : :class:`bool`, `False`
63
        Whether to use a logspaced *grid* on the y axis. Values strictly >0 required.
64
    bins : :class:`int`, 20
65
        Number of bins used in the gridded functions (histograms, KDE evaluation grid).
66
    mode : :class:`str`, 'density'
67
        Different modes used here: ['density', 'hexbin', 'hist2d']
68
    extent : :class:`list`
69
        Predetermined extent of the grid for which to from the histogram/KDE. In the
70
        general form (xmin, xmax, ymin, ymax).
71
    contours : :class:`list`
72
        Contours to add to the plot, where :code:`mode='density'` is used.
73
    percentiles :  :class:`bool`, `True`
74
        Whether contours specified are to be converted to percentiles.
75
    relim : :class:`bool`, :code:`True`
76
        Whether to relimit the plot based on xmin, xmax values.
77
    cmap : :class:`matplotlib.colors.Colormap`
78
        Colormap for mapping surfaces.
79
    vmin : :class:`float`, 0.
80
        Minimum value for colormap.
81
    shading : :class:`str`, 'auto'
82
        Shading to apply to pcolormesh.
83
    colorbar : :class:`bool`, False
84
        Whether to append a linked colorbar to the generated mappable image.
85

86
    {otherparams}
87

88
    Returns
89
    -------
90
    :class:`matplotlib.axes.Axes`
91
        Axes on which the densityplot is plotted.
92

93
    .. seealso::
94

95
        Functions:
96

97
            :func:`matplotlib.pyplot.pcolormesh`
98
            :func:`matplotlib.pyplot.hist2d`
99
            :func:`matplotlib.pyplot.contourf`
100

101
    Notes
102
    -----
103
    The default density estimates and derived contours are generated based on
104
    kernel density estimates. Assumptions around e.g. 95% of points lying within
105
    a 95% contour won't necessarily be valid for non-normally distributed data
106
    (instead, this represents the approximate 95% percentile on the kernel
107
    density estimate). Note that contours are currently only generated; for
108
    `mode="density"`; future updates may allow the use of a histogram
109
    basis, which would give results closer to 95% data percentiles.
110

111
    Todo
112
    ----
113
    * Allow generation of contours from histogram data, rather than just
114
        the kernel density estimate.
115
    * Implement an option and filter to 'scatter' points below the minimum threshold
116
        or maximum percentile contours.
117
    """
118
    if (mode == "density") & np.isclose(vmin, 0.0):  # if vmin is not specified
12✔
119
        vmin = 0.02  # 2% max height | 98th percentile
12✔
120

121
    if arr.shape[-1] == 3:
12✔
122
        projection = "ternary"
12✔
123
    else:
124
        projection = None
12✔
125

126
    ax = init_axes(ax=ax, projection=projection, **kwargs)
12✔
127

128
    pcolor, contour, contourf = get_axis_density_methods(ax)
12✔
129
    background_color = (*ax.patch.get_facecolor()[:-1], 0.0)
12✔
130

131
    if cmap is not None:
12✔
132
        if isinstance(cmap, str):
12✔
133
            cmap = plt.get_cmap(cmap)
12✔
134
        cmap = copy.copy(cmap)  # without this, it would modify the global cmap
12✔
135
        cmap.set_under((1, 1, 1, 0))
12✔
136

137
    if mode == "density":
12✔
138
        cbarlabel = "Kernel Density Estimate"
12✔
139
    else:
140
        cbarlabel = "Frequency"
12✔
141

142
    valid_rows = np.isfinite(arr).all(axis=-1)
12✔
143

144
    if (mode in ["hexbin", "hist2d"]) and contours:
12✔
UNCOV
145
        raise NotImplementedError(
×
146
            "Contours are not currently implemented for 'hexbin' or 'hist2d' modes."
147
        )
148

149
    if (arr.size > 0) and valid_rows.any():
12✔
150
        # Data can't be plotted if there's any nans, so we can exclude these
151
        arr = arr[valid_rows]
12✔
152

153
        if projection is None:  # binary
12✔
154
            x, y = arr.T
12✔
155
            grid = DensityGrid(
12✔
156
                x,
157
                y,
158
                bins=bins,
159
                logx=logx,
160
                logy=logy,
161
                extent=extent,
162
                **subkwargs(kwargs, DensityGrid)
163
            )
164
            if mode == "hexbin":
12✔
165
                # extent values are exponents (i.e. 3 -> 10**3)
166
                mappable = ax.hexbin(
12✔
167
                    x,
168
                    y,
169
                    gridsize=bins,
170
                    cmap=cmap,
171
                    extent=grid.get_hex_extent(),
172
                    xscale=["linear", "log"][logx],
173
                    yscale=["linear", "log"][logy],
174
                    **subkwargs(kwargs, ax.hexbin)
175
                )
176

177
            elif mode == "hist2d":
12✔
178
                _, _, _, im = ax.hist2d(
12✔
179
                    x,
180
                    y,
181
                    bins=[grid.grid_xe, grid.grid_ye],
182
                    range=grid.get_range(),
183
                    cmap=cmap,
184
                    cmin=[0, 1][vmin > 0],
185
                    **subkwargs(kwargs, ax.hist2d)
186
                )
187
                mappable = im
12✔
188

189
            elif mode == "density":
12✔
190
                zei = grid.kdefrom(
12✔
191
                    arr,
192
                    xtransform=[lambda x: x, np.log][logx],
193
                    ytransform=[lambda y: y, np.log][logy],
194
                    mode="edges",
195
                    **subkwargs(kwargs, grid.kdefrom)
196
                )
197

198
                if percentiles:  # 98th percentile
12✔
199
                    vmin = percentile_contour_values_from_meshz(zei, [1.0 - vmin])[1][0]
12✔
200
                    logger.debug(
12✔
201
                        "Updating `vmin` to percentile equiv: {:.2f}".format(vmin)
202
                    )
203

204
                if not contours:
12✔
205
                    # pcolormesh using bin edges
206
                    mappable = pcolor(
12✔
207
                        grid.grid_xei,
208
                        grid.grid_yei,
209
                        zei,
210
                        cmap=cmap,
211
                        vmin=vmin,
212
                        shading=shading,
213
                        **subkwargs(kwargs, pcolor)
214
                    )
215
                    mappable.set_edgecolor(background_color)
12✔
216
                    mappable.set_linestyle("None")
12✔
217
                    mappable.set_lw(0.0)
12✔
218
                else:
219
                    mappable = _add_contours(
12✔
220
                        grid.grid_xei,
221
                        grid.grid_yei,
222
                        zi=zei.reshape(grid.grid_xei.shape),
223
                        ax=ax,
224
                        contours=contours,
225
                        percentiles=percentiles,
226
                        cmap=cmap,
227
                        vmin=vmin,
228
                        **kwargs
229
                    )
230
            if relim and (extent is not None):
12✔
UNCOV
231
                ax.axis(extent)
×
232
        elif projection == "ternary":  # ternary
12✔
233
            if shading == "auto":
12✔
234
                shading = "flat"  # auto cant' be passed to tripcolor
12✔
235
            # zeros make nans in this case, due to the heatmap calculations
236
            arr[~(arr > 0).all(axis=1), :] = np.nan
12✔
237
            arr = close(arr)
12✔
238
            if mode == "hexbin":
12✔
239
                raise NotImplementedError
12✔
240
            # density, histogram etc parsed here
241
            coords, zi, _ = ternary_heatmap(arr, bins=bins, mode=mode)
12✔
242

243
            if percentiles:  # 98th percentile
12✔
244
                vmin = percentile_contour_values_from_meshz(zi, [1.0 - vmin])[1][0]
12✔
245
                logger.debug("Updating `vmin` to percentile equiv: {:.2f}".format(vmin))
12✔
246

247
            # remove coords where H==0, as ax.tripcolor can't deal with variable alpha :'(
248
            fltr = (zi != 0) & (zi >= vmin)
12✔
249
            coords = coords[fltr.flatten(), :]
12✔
250
            zi = zi[fltr]
12✔
251

252
            if not contours:
12✔
253
                tri_poly_collection = pcolor(
12✔
254
                    *coords.T,
255
                    zi.flatten(),
256
                    cmap=cmap,
257
                    vmin=vmin,
258
                    shading=shading,
259
                    **subkwargs(kwargs, pcolor)
260
                )
261

262
                mappable = tri_poly_collection
12✔
263
            else:
264
                mappable = _add_contours(
12✔
265
                    *coords.T,
266
                    zi=zi.flatten(),
267
                    ax=ax,
268
                    contours=contours,
269
                    percentiles=percentiles,
270
                    cmap=cmap,
271
                    vmin=vmin,
272
                    **kwargs
273
                )
274
            ax.set_aspect("equal")
12✔
275
        else:
UNCOV
276
            if not arr.ndim in [0, 1, 2]:
×
UNCOV
277
                raise NotImplementedError
×
278

279
        if colorbar:
12✔
280
            cbkwargs = kwargs.copy()
12✔
281
            cbkwargs["label"] = cbarlabel
12✔
282
            add_colorbar(mappable, **cbkwargs)
12✔
283

284
    return ax
12✔
285

286

287
def _add_contours(
12✔
288
    *coords,
289
    zi=None,
290
    ax=None,
291
    contours=[],
292
    cmap=DEFAULT_CONT_COLORMAP,
293
    vmin=0.0,
294
    extent=None,
295
    **kwargs
296
):
297
    """
298
    Add density-based contours to a plot.
299
    """
300
    # get the contour levels
301
    percentiles = kwargs.pop("percentiles", True)
12✔
302
    levels = contours or kwargs.get("levels", None)
12✔
303
    pcolor, contour, contourf = get_axis_density_methods(ax)
12✔
304
    if percentiles and not isinstance(levels, int):
12✔
305
        # plot individual percentile contours
306
        _cs = plot_Z_percentiles(
12✔
307
            *coords,
308
            zi=zi,
309
            ax=ax,
310
            percentiles=levels,
311
            extent=extent,
312
            cmap=cmap,
313
            **kwargs
314
        )
315
        mappable = _cs
12✔
316
    else:
317
        # plot interval contours
318
        if levels is None:
12✔
UNCOV
319
            levels = MaxNLocator(nbins=10).tick_values(zi.min(), zi.max())
×
320
        elif isinstance(levels, int):
12✔
321
            levels = MaxNLocator(nbins=levels).tick_values(zi.min(), zi.max())
12✔
322
        else:
UNCOV
323
            raise NotImplementedError
×
324
        # filled contours
325
        mappable = contourf(
12✔
326
            *coords, zi, extent=extent, levels=levels, cmap=cmap, vmin=vmin, **kwargs
327
        )
328
        # contours
329
        contour(
12✔
330
            *coords, zi, extent=extent, levels=levels, cmap=cmap, vmin=vmin, **kwargs
331
        )
332
    return mappable
12✔
333

334

335
_add_additional_parameters = True
12✔
336

337
density.__doc__ = density.__doc__.format(
12✔
338
    otherparams=[
339
        "",
340
        get_additional_params(
341
            density,
342
            plt.pcolormesh,
343
            plt.hist2d,
344
            plt.hexbin,
345
            plt.contour,
346
            plt.contourf,
347
            header="Other Parameters",
348
            indent=4,
349
            subsections=True,
350
        ),
351
    ][_add_additional_parameters]
352
)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc