• 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

94.16
/pyrolite/plot/color.py
1
import copy
12✔
2

3
import matplotlib.colors
12✔
4
import matplotlib.pyplot as plt
12✔
5
import numpy as np
12✔
6
import pandas as pd
12✔
7

8
from ..util.log import Handle
12✔
9
from ..util.plot import DEFAULT_CONT_COLORMAP, DEFAULT_DISC_COLORMAP
12✔
10

11
logger = Handle(__name__)
12✔
12

13
_face_edge_equivalents = {
12✔
14
    "facecolors": "edgecolors",
15
    "markerfacecolor": "markeredgecolor",
16
    "mfc": "mec",
17
}
18

19

20
def get_cmode(c=None):
12✔
21
    """
22
    Find which mode a color is supplied as, such that it can be processed.
23

24
    Parameters
25
    -----------
26
    c :  :class:`str` | :class:`list` | :class:`tuple` | :class:`numpy.ndarray`
27
        Color arguments as typically passed to :func:`matplotlib.pyplot.scatter`
28
        or :func:`matplotlib.pyplot.plot`.
29
    """
30
    cmode = None
12✔
31
    if c is not None:  # named | hex | rgb | rgba
12✔
32
        logger.debug("Checking singular color modes.")
12✔
33
        if isinstance(c, str):
12✔
34
            if c.startswith("#"):
12✔
35
                cmode = "hex"
12✔
36
            else:
37
                cmode = "named"
12✔
38
        elif isinstance(c, tuple):
12✔
39
            if len(c) == 3:
12✔
40
                cmode = "rgb"
12✔
41
            elif len(c) == 4:
12✔
42
                cmode = "rgba"
12✔
43
        else:
44
            pass
6✔
45

46
        if cmode is None:  # list | ndarray | ndarray(rgb) | ndarray(rgba)
12✔
47
            logger.debug("Checking array-based color modes.")
12✔
48
            if isinstance(
12✔
49
                c,
50
                (
51
                    np.ndarray,
52
                    list,
53
                    pd.Series,
54
                    pd.Index,
55
                    pd.Categorical,
56
                ),
57
            ):
58
                dtype = getattr(c, "dtype", np.dtype("O"))
12✔
59
                if dtype.name == "category":  # convert categories to objects for numpy
12✔
UNCOV
60
                    dtype = np.dtype("O")
×
61
                c = np.array(c, dtype=dtype)
12✔
62
                convertible = False
12✔
63
                try:  # could test all of them, or just a few
12✔
64
                    _ = [matplotlib.colors.to_rgba(_c) for _c in [c[0], c[-1]]]
12✔
65
                    convertible = True
12✔
66
                except (ValueError, TypeError):  # string cannot be converted to color
12✔
67
                    pass
12✔
68
                if all([isinstance(_c, (np.ndarray, list, tuple)) for _c in c]):
12✔
69
                    # could have an error if you put in mixed rgb/rgba
70
                    if len(c[0]) == 3:
12✔
71
                        cmode = "rgb_array"
12✔
72
                    elif len(c[0]) == 4:
12✔
73
                        cmode = "rgba_array"
12✔
74
                    else:
75
                        pass
6✔
76
                elif all([isinstance(_c, str) for _c in c]):
12✔
77
                    if convertible:
12✔
78
                        if all([_c.startswith("#") for _c in c]):
12✔
79
                            cmode = "hex_array"
12✔
80
                        elif not any([_c.startswith("#") for _c in c]):
12✔
81
                            cmode = "named_array"
12✔
82
                        else:
83
                            cmode = "mixed_str_array"
12✔
84
                    else:
85
                        cmode = "categories"
12✔
86
                elif all([isinstance(_c, np.number) for _c in np.array(c).flatten()]):
12✔
87
                    cmode = "value_array"
12✔
88
                else:
89
                    if convertible:
12✔
90
                        cmode = "mixed_fmt_color_array"
12✔
91
                if cmode is None:
12✔
92
                    # default cmode to fall back on - e.g. list of tuples/intervals etc
93
                    # where they're all the same type
94
                    types = {type(_c) for _c in set(c)}
12✔
95
                    if len(types) == 1:
12✔
96
                        cmode = "categories"
12✔
97
                    else:
98
                        raise NotImplementedError(
12✔
99
                            "Cannot determine color mode from array including types {}.".format(
100
                                ",".join([t.__name__ for t in types])
101
                            )
102
                        )
103
    if cmode is None:
12✔
104
        msg = "Color mode not found for item of type {}".format(type(c))
12✔
105
        logger.debug(msg)
12✔
106
        raise NotImplementedError(msg)  # single value, mixed numbers, strings etc
12✔
107
    else:
108
        logger.debug("Color mode recognized: {}".format(cmode))
12✔
109
        return cmode
12✔
110

111

112
def process_color(
12✔
113
    c=None,
114
    color=None,
115
    cmap=None,
116
    alpha=None,
117
    norm=None,
118
    bad="0.5",
119
    cmap_under=(1, 1, 1, 0.0),
120
    color_converter=matplotlib.colors.to_rgba,
121
    color_mappings={},
122
    size=None,
123
    **otherkwargs,
124
):
125
    """
126
    Color argument processing for pyrolite plots, returning a standardised output.
127

128
    Parameters
129
    -----------
130
    c : :class:`str` | :class:`list` | :class:`tuple` | :class:`numpy.ndarray`
131
        Color arguments as typically passed to :func:`matplotlib.pyplot.scatter`.
132
    color : :class:`str` | :class:`list` | :class:`tuple` | :class:`numpy.ndarray`
133
        Color arguments as typically passed to :func:`matplotlib.pyplot.plot`
134
    cmap : :class:`str` | :class:`~matplotlib.cm.ScalarMappable`
135
        Colormap for mapping unknown color values.
136
    alpha : :class:`float`
137
        Alpha to modulate color opacity.
138
    norm : :class:`~matplotlib.colors.Normalize`
139
        Normalization for the colormap.
140
    cmap_under : :class:`str` | :class:`tuple`
141
        Color for values below the lower threshold for the cmap.
142
    color_converter
143
        Function to use to convert colors (from strings, hex, tuples etc).
144
    color_mappings : :class:`dict`
145
        Dictionary containing category-color mappings for individual color variables,
146
        with the default color mapping having the key 'color'. For use where
147
        categorical values are specified for a color variable.
148
    size : :class:`int`
149
        Size of the data array along the first axis.
150

151
    Returns
152
    --------
153
    C : :class:`tuple` | :class:`numpy.ndarray`
154
        Color returned in standardised RGBA format.
155

156
    Notes
157
    ------
158
    As formulated here, the addition of unused styling parameters may cause some
159
    properties (associated with 'c') to be set to None - and hence revert to defaults.
160
    This might be mitigated if the context could be checked - e.g. via checking
161
    keyword argument membership of :func:`~pyrolite.util.plot.style.scatterkwargs` etc.
162
    """
163
    assert not ((c is not None) and (color is not None))
12✔
164
    for kw in [  # extra color kwargs
12✔
165
        "facecolors",
166
        "markerfacecolor",
167
        "mfc",
168
        "markeredgecolor",
169
        "mec",
170
        "edgecolors",
171
        "ec",
172
        "linecolor",
173
        "lc",
174
        "ecolor",  # for errobar
175
        "facecolor",
176
    ]:
177
        if kw in otherkwargs:  # this allows processing of alpha with a given color
12✔
178
            _pc = process_color(
12✔
179
                c=otherkwargs[kw],
180
                alpha=alpha,
181
                cmap=cmap,
182
                norm=norm,
183
                color_mappings={"c": color_mappings.get(kw)},
184
            )
185
            otherkwargs[kw] = _pc.get("c")
12✔
186
    if c is not None:
12✔
187
        C = c
12✔
188
    elif color is not None:
12✔
189
        C = color
12✔
190
    else:  # neither color is specified
191
        d = {
12✔
192
            **{
193
                k: v
194
                for k, v in {
195
                    "c": c,
196
                    "color": color,
197
                    "cmap": cmap,
198
                    "norm": norm,
199
                    "alpha": alpha,
200
                }.items()
201
                if v is not None
202
            },
203
            **otherkwargs,
204
        }
205
        # the parameter 'c' will override 'facecolor' and related
206
        if any([k in d for k in _face_edge_equivalents.keys()]):
12✔
207
            d.pop("c", None)
12✔
208
        return d
12✔
209

210
    cmode = get_cmode(C)
12✔
211

212
    _c, _color = None, None
12✔
213
    if cmode in ["hex", "named", "rgb", "rgba"]:  # single color
12✔
214
        C = matplotlib.colors.to_rgba(C)
12✔
215
        if alpha is not None:
12✔
216
            C = (
12✔
217
                *C[:-1],
218
                alpha * C[-1],
219
            )  # can't assign to tuple, create new one instead
220
        _c, _color = np.array([C]), C  # Convert to standardised form
12✔
221
        if size is not None:
12✔
222
            _c = np.ones((size, 1)) * _c  # turn this into a full array as a fallback
12✔
223
    else:
224
        if cmode in [
12✔
225
            "hex_array",
226
            "named_array",
227
            "mixed_str_array",
228
        ]:
229
            C = np.array([matplotlib.colors.to_rgba(ic) for ic in C])
12✔
230
        elif cmode in ["rgb_array", "rgba_array"]:
12✔
231
            C = np.array([matplotlib.colors.to_rgba(ic) for ic in C])
12✔
232
        elif cmode in ["mixed_fmt_color_array"]:
12✔
233
            C = np.array([matplotlib.colors.to_rgba(ic) for ic in C])
12✔
234
        elif cmode in ["value_array"]:
12✔
235
            _C = np.array(C)
12✔
236
            cmap = cmap or DEFAULT_CONT_COLORMAP
12✔
237
            if isinstance(cmap, str):
12✔
238
                cmap = plt.get_cmap(cmap)
12✔
239
            if cmap_under is not None:
12✔
240
                cmap = copy.copy(cmap)  # without this, it would modify the global cmap
12✔
241
                cmap.set_under(color=cmap_under)
12✔
242
            norm = norm or plt.Normalize(
12✔
243
                vmin=otherkwargs.get("vmin") or np.nanmin(_C),
244
                vmax=otherkwargs.get("vmax") or np.nanmax(_C),
245
            )
246
            C = cmap(norm(_C))
12✔
247
        elif cmode == "categories":
12✔
248
            C = np.array(C, dtype="object")
12✔
249
            uniqueC = pd.unique(C)
12✔
250
            # this should now work for 'c' in addition to 'color', where the notation is matching
251
            cmapper = (
12✔
252
                color_mappings.get("c")
253
                if c is not None
254
                else color_mappings.get("color")
255
            )
256
            if cmapper is None:
12✔
257
                logger.debug("Using default value-mapping for categories.")
12✔
258
                _C = np.ones(len(C), dtype="int") * np.nan
12✔
259

260
                cmap = cmap or DEFAULT_DISC_COLORMAP
12✔
261
                if isinstance(cmap, str):
12✔
UNCOV
262
                    cmap = plt.get_cmap(cmap)
×
263

264
                for ix, cat in enumerate(uniqueC):
12✔
265
                    _C[C == cat] = ix / len(uniqueC)
12✔
266
                C = cmap(_C)
12✔
267
            else:
268
                logger.debug("Using custom value-mapping for categories.")
12✔
269
                C = np.array(C)
12✔
270
                _C = np.ones((len(C), 4), dtype=float)
12✔
271
                for cat in uniqueC:
12✔
272
                    # subsitute in the 'bad' color for colors not in the cmap
273
                    val = matplotlib.colors.to_rgba(cmapper.get(cat, bad))
12✔
274
                    _C[C == cat] = val  # get the mapping frome the dict
12✔
275
                C = _C
12✔
276
        else:
UNCOV
277
            C = np.array(C)
×
278
        if alpha is not None:
12✔
279
            C[:, -1] = alpha
12✔
280
        _c, _color = C, C
12✔
281

282
    d = {"color": _color, **otherkwargs}
12✔
283
    # the parameter 'c' will override 'facecolors' and related for markers
284
    if not any(
12✔
285
        [
286
            k in d
287
            for k in [item for args in _face_edge_equivalents.items() for item in args]
288
        ]
289
    ):
290
        d["c"] = _c
12✔
291
    else:
292
        # for each of the facecolor modes specified return an edge variant
UNCOV
293
        for face, edge in _face_edge_equivalents.items():
×
UNCOV
294
            if (face in d) and not (edge in d):
×
UNCOV
295
                d[edge] = _c
×
UNCOV
296
            if (edge in d) and not (face in d):
×
UNCOV
297
                d[face] = _c
×
298
    return d
12✔
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