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

colour-science / colour / 18370061804

09 Oct 2025 08:19AM UTC coverage: 76.753% (-22.6%) from 99.349%
18370061804

push

github

KelSolaar
Merge branch 'feature/v0.4.7' into develop

32663 of 42556 relevant lines covered (76.75%)

0.77 hits per line

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

68.29
/colour/plotting/quality.py
1
"""
2
Colour Quality Plotting
3
=======================
4

5
Define the colour quality plotting objects.
6

7
-   :func:`colour.plotting.plot_single_sd_colour_rendering_index_bars`
8
-   :func:`colour.plotting.plot_multi_sds_colour_rendering_indexes_bars`
9
-   :func:`colour.plotting.plot_single_sd_colour_quality_scale_bars`
10
-   :func:`colour.plotting.plot_multi_sds_colour_quality_scales_bars`
11
"""
12

13
from __future__ import annotations
×
14

15
import typing
×
16

17
if typing.TYPE_CHECKING:
18
    from collections.abc import ValuesView
19

20
from itertools import cycle
×
21

22
if typing.TYPE_CHECKING:
23
    from matplotlib.figure import Figure
24
    from matplotlib.axes import Axes
25

26
import numpy as np
×
27

28
from colour.colorimetry import (
×
29
    MultiSpectralDistributions,
30
    SpectralDistribution,
31
    sds_and_msds_to_sds,
32
)
33
from colour.constants import DTYPE_FLOAT_DEFAULT
×
34

35
if typing.TYPE_CHECKING:
36
    from colour.hints import (
37
        Any,
38
        Dict,
39
        Literal,
40
        Sequence,
41
        Tuple,
42
    )
43

44
from colour.plotting import (
×
45
    CONSTANTS_COLOUR_STYLE,
46
    XYZ_to_plotting_colourspace,
47
    artist,
48
    label_rectangles,
49
    override_style,
50
    render,
51
)
52
from colour.quality import (
×
53
    COLOUR_QUALITY_SCALE_METHODS,
54
    ColourRendering_Specification_CQS,
55
    ColourRendering_Specification_CRI,
56
    colour_quality_scale,
57
    colour_rendering_index,
58
)
59
from colour.utilities import as_float_array, ones, validate_method
×
60

61
__author__ = "Colour Developers"
×
62
__copyright__ = "Copyright 2013 Colour Developers"
×
63
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
×
64
__maintainer__ = "Colour Developers"
×
65
__email__ = "colour-developers@colour-science.org"
×
66
__status__ = "Production"
×
67

68
__all__ = [
×
69
    "plot_colour_quality_bars",
70
    "plot_single_sd_colour_rendering_index_bars",
71
    "plot_multi_sds_colour_rendering_indexes_bars",
72
    "plot_single_sd_colour_quality_scale_bars",
73
    "plot_multi_sds_colour_quality_scales_bars",
74
]
75

76

77
@override_style()
×
78
def plot_colour_quality_bars(
×
79
    specifications: Sequence[
80
        ColourRendering_Specification_CQS | ColourRendering_Specification_CRI
81
    ],
82
    labels: bool = True,
83
    hatching: bool | None = None,
84
    hatching_repeat: int = 2,
85
    **kwargs: Any,
86
) -> Tuple[Figure, Axes]:
87
    """
88
    Plot the colour quality data of the specified illuminants or light sources
89
    colour quality specifications.
90

91
    Parameters
92
    ----------
93
    specifications
94
        Array of illuminants or light sources colour quality
95
        specifications.
96
    labels
97
        Add labels above bars.
98
    hatching
99
        Use hatching for the bars.
100
    hatching_repeat
101
        Hatching pattern repeat.
102

103
    Other Parameters
104
    ----------------
105
    kwargs
106
        {:func:`colour.plotting.artist`,
107
        :func:`colour.plotting.quality.plot_colour_quality_bars`,
108
        :func:`colour.plotting.render`},
109
        See the documentation of the previously listed definitions.
110

111
    Returns
112
    -------
113
    :class:`tuple`
114
        Current figure and axes.
115

116
    Examples
117
    --------
118
    >>> from colour import SDS_ILLUMINANTS, SDS_LIGHT_SOURCES, SpectralShape
119
    >>> illuminant = SDS_ILLUMINANTS["FL2"]
120
    >>> light_source = SDS_LIGHT_SOURCES["Kinoton 75P"]
121
    >>> light_source = light_source.copy().align(SpectralShape(360, 830, 1))
122
    >>> cqs_i = colour_quality_scale(illuminant, additional_data=True)
123
    >>> cqs_l = colour_quality_scale(light_source, additional_data=True)
124
    >>> plot_colour_quality_bars([cqs_i, cqs_l])  # doctest: +ELLIPSIS
125
    (<Figure size ... with 1 Axes>, <...Axes...>)
126

127
    .. image:: ../_static/Plotting_Plot_Colour_Quality_Bars.png
128
        :align: center
129
        :alt: plot_colour_quality_bars
130
    """
131

132
    settings: Dict[str, Any] = {"uniform": True}
1✔
133
    settings.update(kwargs)
1✔
134

135
    _figure, axes = artist(**settings)
1✔
136

137
    bar_width = 0.5
1✔
138
    y_ticks_interval = 10
1✔
139
    count_s, count_Q_as = len(specifications), 0
1✔
140
    patterns = cycle(CONSTANTS_COLOUR_STYLE.hatch.patterns)
1✔
141
    if hatching is None:
1✔
142
        hatching = count_s != 1
1✔
143

144
    for i, specification in enumerate(specifications):
1✔
145
        Q_a, Q_as, colorimetry_data = (
1✔
146
            specification.Q_a,
147
            specification.Q_as,
148
            specification.colorimetry_data,
149
        )
150

151
        count_Q_as = len(Q_as)
1✔
152
        RGB = [ones(3)] + [
1✔
153
            np.clip(XYZ_to_plotting_colourspace(x.XYZ), 0, 1)
154
            for x in colorimetry_data[0]
155
        ]
156

157
        x = (
1✔
158
            as_float_array(
159
                i
160
                + np.arange(
161
                    0,
162
                    (count_Q_as + 1) * (count_s + 1),
163
                    (count_s + 1),
164
                    dtype=DTYPE_FLOAT_DEFAULT,
165
                )
166
            )
167
            * bar_width
168
        )
169
        y = as_float_array(
1✔
170
            [Q_a] + [s[1].Q_a for s in sorted(Q_as.items(), key=lambda s: s[0])]
171
        )
172

173
        bars = axes.bar(
1✔
174
            x,
175
            np.abs(y),
176
            color=RGB,
177
            width=bar_width,
178
            edgecolor=CONSTANTS_COLOUR_STYLE.colour.dark,
179
            label=specification.name,
180
            zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
181
        )
182

183
        hatches = (
1✔
184
            [next(patterns) * hatching_repeat] * (count_Q_as + 1)
185
            if hatching
186
            else list(np.where(y < 0, next(patterns), None))  # pyright: ignore
187
        )
188

189
        for j, bar in enumerate(bars.patches):
1✔
190
            bar.set_hatch(hatches[j])
1✔
191

192
        if labels:
1✔
193
            label_rectangles(
1✔
194
                [f"{y_v:.1f}" for y_v in y],
195
                bars,
196
                rotation="horizontal" if count_s == 1 else "vertical",
197
                offset=(
198
                    0 if count_s == 1 else 3 / 100 * count_s + 65 / 1000,
199
                    0.025,
200
                ),
201
                text_size=-5 / 7 * count_s + 12.5,
202
                axes=axes,
203
            )
204

205
    axes.axhline(
1✔
206
        y=100,
207
        color=CONSTANTS_COLOUR_STYLE.colour.dark,
208
        linestyle="--",
209
        zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
210
    )
211

212
    axes.set_xticks(
1✔
213
        (
214
            np.arange(
215
                0,
216
                (count_Q_as + 1) * (count_s + 1),
217
                (count_s + 1),
218
                dtype=DTYPE_FLOAT_DEFAULT,
219
            )
220
            - bar_width
221
        )
222
        * bar_width
223
        + (count_s * bar_width / 2)
224
    )
225
    axes.set_xticklabels(
1✔
226
        ["Qa"] + [f"Q{index + 1}" for index in range(0, count_Q_as, 1)]
227
    )
228
    axes.set_yticks(range(0, 100 + y_ticks_interval, y_ticks_interval))
1✔
229

230
    aspect = 1 / (120 / (bar_width + len(Q_as) + bar_width * 2))
1✔
231
    bounding_box = (
1✔
232
        -bar_width,
233
        ((count_Q_as + 1) * (count_s + 1)) / 2 - bar_width,
234
        0,
235
        120,
236
    )
237

238
    settings = {
1✔
239
        "axes": axes,
240
        "aspect": aspect,
241
        "bounding_box": bounding_box,
242
        "legend": hatching,
243
        "title": "Colour Quality",
244
    }
245
    settings.update(kwargs)
1✔
246

247
    return render(**settings)
1✔
248

249

250
@override_style()
×
251
def plot_single_sd_colour_rendering_index_bars(
×
252
    sd: SpectralDistribution, **kwargs: Any
253
) -> Tuple[Figure, Axes]:
254
    """
255
    Plot the *Colour Rendering Index* (CRI) of the specified illuminant or
256
    light source spectral distribution.
257

258
    Parameters
259
    ----------
260
    sd
261
        Illuminant or light source spectral distribution for which to plot
262
        the *Colour Rendering Index* (CRI).
263

264
    Other Parameters
265
    ----------------
266
    kwargs
267
        {:func:`colour.plotting.artist`,
268
        :func:`colour.plotting.quality.plot_colour_quality_bars`,
269
        :func:`colour.plotting.render`},
270
        See the documentation of the previously listed definitions.
271

272
    Returns
273
    -------
274
    :class:`tuple`
275
        Current figure and axes.
276

277
    Examples
278
    --------
279
    >>> from colour import SDS_ILLUMINANTS
280
    >>> illuminant = SDS_ILLUMINANTS["FL2"]
281
    >>> plot_single_sd_colour_rendering_index_bars(illuminant)
282
    ... # doctest: +ELLIPSIS
283
    (<Figure size ... with 1 Axes>, <...Axes...>)
284

285
    .. image:: ../_static/Plotting_\
286
Plot_Single_SD_Colour_Rendering_Index_Bars.png
287
        :align: center
288
        :alt: plot_single_sd_colour_rendering_index_bars
289
    """
290

291
    return plot_multi_sds_colour_rendering_indexes_bars([sd], **kwargs)
1✔
292

293

294
@override_style()
×
295
def plot_multi_sds_colour_rendering_indexes_bars(
×
296
    sds: (
297
        Sequence[SpectralDistribution | MultiSpectralDistributions]
298
        | SpectralDistribution
299
        | MultiSpectralDistributions
300
        | ValuesView
301
    ),
302
    **kwargs: Any,
303
) -> Tuple[Figure, Axes]:
304
    """
305
    Plot the *Colour Rendering Index* (CRI) of the specified illuminants or
306
    light sources spectral distributions.
307

308
    Parameters
309
    ----------
310
    sds
311
        Spectral distributions or multi-spectral distributions to plot.
312
        `sds` can be a single :class:`colour.MultiSpectralDistributions`
313
        class instance, a list of  :class:`colour.MultiSpectralDistributions`
314
        class instances or a list of :class:`colour.SpectralDistribution` class
315
        instances.
316

317
    Other Parameters
318
    ----------------
319
    kwargs
320
        {:func:`colour.plotting.artist`,
321
        :func:`colour.plotting.quality.plot_colour_quality_bars`,
322
        :func:`colour.plotting.render`},
323
        See the documentation of the previously listed definitions.
324

325
    Returns
326
    -------
327
    :class:`tuple`
328
        Current figure and axes.
329

330
    Examples
331
    --------
332
    >>> from colour import SDS_ILLUMINANTS, SDS_LIGHT_SOURCES
333
    >>> illuminant = SDS_ILLUMINANTS["FL2"]
334
    >>> light_source = SDS_LIGHT_SOURCES["Kinoton 75P"]
335
    >>> plot_multi_sds_colour_rendering_indexes_bars(
336
    ...     [illuminant, light_source]
337
    ... )  # doctest: +ELLIPSIS
338
    (<Figure size ... with 1 Axes>, <...Axes...>)
339

340
    .. image:: ../_static/Plotting_\
341
Plot_Multi_SDS_Colour_Rendering_Indexes_Bars.png
342
        :align: center
343
        :alt: plot_multi_sds_colour_rendering_indexes_bars
344
    """
345

346
    sds_converted = sds_and_msds_to_sds(sds)
1✔
347

348
    settings: Dict[str, Any] = dict(kwargs)
1✔
349
    settings.update({"show": False})
1✔
350

351
    specifications = [
1✔
352
        colour_rendering_index(sd, additional_data=True) for sd in sds_converted
353
    ]
354

355
    # *colour rendering index* colorimetry data tristimulus values are
356
    # computed in [0, 100] domain however `plot_colour_quality_bars` expects
357
    # [0, 1] domain. As we want to keep `plot_colour_quality_bars` definition
358
    # agnostic from the colour quality data, we update the test sd
359
    # colorimetry data tristimulus values domain.
360
    for specification in specifications:
1✔
361
        colorimetry_data = specification.colorimetry_data
1✔
362
        for i in range(len(colorimetry_data[0])):
1✔
363
            colorimetry_data[0][i].XYZ /= 100
1✔
364

365
    _figure, axes = plot_colour_quality_bars(specifications, **settings)
1✔
366

367
    title = (
1✔
368
        f"Colour Rendering Index - "
369
        f"{', '.join([sd.display_name for sd in sds_converted])}"
370
    )
371

372
    settings = {"axes": axes, "title": title}
1✔
373
    settings.update(kwargs)
1✔
374

375
    return render(**settings)
1✔
376

377

378
@override_style()
×
379
def plot_single_sd_colour_quality_scale_bars(
×
380
    sd: SpectralDistribution,
381
    method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = "NIST CQS 9.0",
382
    **kwargs: Any,
383
) -> Tuple[Figure, Axes]:
384
    """
385
    Plot the *Colour Quality Scale* (CQS) of the specified illuminant or
386
    light source spectral distribution.
387

388
    Parameters
389
    ----------
390
    sd
391
        Illuminant or light source spectral distribution for which to plot
392
        the *Colour Quality Scale* (CQS).
393
    method
394
        *Colour Quality Scale* (CQS) computation method.
395

396
    Other Parameters
397
    ----------------
398
    kwargs
399
        {:func:`colour.plotting.artist`,
400
        :func:`colour.plotting.quality.plot_colour_quality_bars`,
401
        :func:`colour.plotting.render`},
402
        See the documentation of the previously listed definitions.
403

404
    Returns
405
    -------
406
    :class:`tuple`
407
        Current figure and axes.
408

409
    Examples
410
    --------
411
    >>> from colour import SDS_ILLUMINANTS
412
    >>> illuminant = SDS_ILLUMINANTS["FL2"]
413
    >>> plot_single_sd_colour_quality_scale_bars(illuminant)
414
    ... # doctest: +ELLIPSIS
415
    (<Figure size ... with 1 Axes>, <...Axes...>)
416

417
    .. image:: ../_static/Plotting_\
418
Plot_Single_SD_Colour_Quality_Scale_Bars.png
419
        :align: center
420
        :alt: plot_single_sd_colour_quality_scale_bars
421
    """
422

423
    method = validate_method(method, tuple(COLOUR_QUALITY_SCALE_METHODS))
1✔
424

425
    return plot_multi_sds_colour_quality_scales_bars([sd], method, **kwargs)
1✔
426

427

428
@override_style()
×
429
def plot_multi_sds_colour_quality_scales_bars(
×
430
    sds: (
431
        Sequence[SpectralDistribution | MultiSpectralDistributions]
432
        | SpectralDistribution
433
        | MultiSpectralDistributions
434
        | ValuesView
435
    ),
436
    method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = "NIST CQS 9.0",
437
    **kwargs: Any,
438
) -> Tuple[Figure, Axes]:
439
    """
440
    Plot the *Colour Quality Scale* (CQS) of the specified illuminants or light
441
    sources spectral distributions.
442

443
    Parameters
444
    ----------
445
    sds
446
        Spectral distributions or multi-spectral distributions to plot.
447
        `sds` can be a single :class:`colour.MultiSpectralDistributions`
448
        class instance, a list of  :class:`colour.MultiSpectralDistributions`
449
        class instances or a list of :class:`colour.SpectralDistribution` class
450
        instances.
451
    method
452
        *Colour Quality Scale* (CQS) computation method.
453

454
    Other Parameters
455
    ----------------
456
    kwargs
457
        {:func:`colour.plotting.artist`,
458
        :func:`colour.plotting.quality.plot_colour_quality_bars`,
459
        :func:`colour.plotting.render`},
460
        See the documentation of the previously listed definitions.
461

462
    Returns
463
    -------
464
    :class:`tuple`
465
        Current figure and axes.
466

467
    Examples
468
    --------
469
    >>> from colour import SDS_ILLUMINANTS, SDS_LIGHT_SOURCES
470
    >>> illuminant = SDS_ILLUMINANTS["FL2"]
471
    >>> light_source = SDS_LIGHT_SOURCES["Kinoton 75P"]
472
    >>> plot_multi_sds_colour_quality_scales_bars([illuminant, light_source])
473
    ... # doctest: +ELLIPSIS
474
    (<Figure size ... with 1 Axes>, <...Axes...>)
475

476
    .. image:: ../_static/Plotting_\
477
Plot_Multi_SDS_Colour_Quality_Scales_Bars.png
478
        :align: center
479
        :alt: plot_multi_sds_colour_quality_scales_bars
480
    """
481

482
    method = validate_method(method, tuple(COLOUR_QUALITY_SCALE_METHODS))
1✔
483

484
    sds_converted = sds_and_msds_to_sds(sds)
1✔
485

486
    settings: Dict[str, Any] = dict(kwargs)
1✔
487
    settings.update({"show": False})
1✔
488

489
    specifications = [colour_quality_scale(sd, True, method) for sd in sds_converted]
1✔
490

491
    _figure, axes = plot_colour_quality_bars(specifications, **settings)
1✔
492

493
    title = (
1✔
494
        f"Colour Quality Scale - {', '.join([sd.display_name for sd in sds_converted])}"
495
    )
496

497
    settings = {"axes": axes, "title": title}
1✔
498
    settings.update(kwargs)
1✔
499

500
    return render(**settings)
1✔
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