Coverage for plotting/quality.py: 68%
82 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Colour Quality Plotting
3=======================
5Define the colour quality plotting objects.
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"""
13from __future__ import annotations
15import typing
17if typing.TYPE_CHECKING:
18 from collections.abc import ValuesView
20from itertools import cycle
22if typing.TYPE_CHECKING:
23 from matplotlib.figure import Figure
24 from matplotlib.axes import Axes
26import numpy as np
28from colour.colorimetry import (
29 MultiSpectralDistributions,
30 SpectralDistribution,
31 sds_and_msds_to_sds,
32)
33from colour.constants import DTYPE_FLOAT_DEFAULT
35if typing.TYPE_CHECKING:
36 from colour.hints import (
37 Any,
38 Dict,
39 Literal,
40 Sequence,
41 Tuple,
42 )
44from colour.plotting import (
45 CONSTANTS_COLOUR_STYLE,
46 XYZ_to_plotting_colourspace,
47 artist,
48 label_rectangles,
49 override_style,
50 render,
51)
52from 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)
59from colour.utilities import as_float_array, ones, validate_method
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"
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]
77@override_style()
78def 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.
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.
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.
111 Returns
112 -------
113 :class:`tuple`
114 Current figure and axes.
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...>)
127 .. image:: ../_static/Plotting_Plot_Colour_Quality_Bars.png
128 :align: center
129 :alt: plot_colour_quality_bars
130 """
132 settings: Dict[str, Any] = {"uniform": True}
133 settings.update(kwargs)
135 _figure, axes = artist(**settings)
137 bar_width = 0.5
138 y_ticks_interval = 10
139 count_s, count_Q_as = len(specifications), 0
140 patterns = cycle(CONSTANTS_COLOUR_STYLE.hatch.patterns)
141 if hatching is None:
142 hatching = count_s != 1
144 for i, specification in enumerate(specifications):
145 Q_a, Q_as, colorimetry_data = (
146 specification.Q_a,
147 specification.Q_as,
148 specification.colorimetry_data,
149 )
151 count_Q_as = len(Q_as)
152 RGB = [ones(3)] + [
153 np.clip(XYZ_to_plotting_colourspace(x.XYZ), 0, 1)
154 for x in colorimetry_data[0]
155 ]
157 x = (
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(
170 [Q_a] + [s[1].Q_a for s in sorted(Q_as.items(), key=lambda s: s[0])]
171 )
173 bars = axes.bar(
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 )
183 hatches = (
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 )
189 for j, bar in enumerate(bars.patches):
190 bar.set_hatch(hatches[j])
192 if labels:
193 label_rectangles(
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 )
205 axes.axhline(
206 y=100,
207 color=CONSTANTS_COLOUR_STYLE.colour.dark,
208 linestyle="--",
209 zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
210 )
212 axes.set_xticks(
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(
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))
230 aspect = 1 / (120 / (bar_width + len(Q_as) + bar_width * 2))
231 bounding_box = (
232 -bar_width,
233 ((count_Q_as + 1) * (count_s + 1)) / 2 - bar_width,
234 0,
235 120,
236 )
238 settings = {
239 "axes": axes,
240 "aspect": aspect,
241 "bounding_box": bounding_box,
242 "legend": hatching,
243 "title": "Colour Quality",
244 }
245 settings.update(kwargs)
247 return render(**settings)
250@override_style()
251def 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.
258 Parameters
259 ----------
260 sd
261 Illuminant or light source spectral distribution for which to plot
262 the *Colour Rendering Index* (CRI).
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.
272 Returns
273 -------
274 :class:`tuple`
275 Current figure and axes.
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...>)
285 .. image:: ../_static/Plotting_\
286Plot_Single_SD_Colour_Rendering_Index_Bars.png
287 :align: center
288 :alt: plot_single_sd_colour_rendering_index_bars
289 """
291 return plot_multi_sds_colour_rendering_indexes_bars([sd], **kwargs)
294@override_style()
295def 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.
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.
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.
325 Returns
326 -------
327 :class:`tuple`
328 Current figure and axes.
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...>)
340 .. image:: ../_static/Plotting_\
341Plot_Multi_SDS_Colour_Rendering_Indexes_Bars.png
342 :align: center
343 :alt: plot_multi_sds_colour_rendering_indexes_bars
344 """
346 sds_converted = sds_and_msds_to_sds(sds)
348 settings: Dict[str, Any] = dict(kwargs)
349 settings.update({"show": False})
351 specifications = [
352 colour_rendering_index(sd, additional_data=True) for sd in sds_converted
353 ]
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:
361 colorimetry_data = specification.colorimetry_data
362 for i in range(len(colorimetry_data[0])):
363 colorimetry_data[0][i].XYZ /= 100
365 _figure, axes = plot_colour_quality_bars(specifications, **settings)
367 title = (
368 f"Colour Rendering Index - "
369 f"{', '.join([sd.display_name for sd in sds_converted])}"
370 )
372 settings = {"axes": axes, "title": title}
373 settings.update(kwargs)
375 return render(**settings)
378@override_style()
379def 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.
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.
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.
404 Returns
405 -------
406 :class:`tuple`
407 Current figure and axes.
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...>)
417 .. image:: ../_static/Plotting_\
418Plot_Single_SD_Colour_Quality_Scale_Bars.png
419 :align: center
420 :alt: plot_single_sd_colour_quality_scale_bars
421 """
423 method = validate_method(method, tuple(COLOUR_QUALITY_SCALE_METHODS))
425 return plot_multi_sds_colour_quality_scales_bars([sd], method, **kwargs)
428@override_style()
429def 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.
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.
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.
462 Returns
463 -------
464 :class:`tuple`
465 Current figure and axes.
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...>)
476 .. image:: ../_static/Plotting_\
477Plot_Multi_SDS_Colour_Quality_Scales_Bars.png
478 :align: center
479 :alt: plot_multi_sds_colour_quality_scales_bars
480 """
482 method = validate_method(method, tuple(COLOUR_QUALITY_SCALE_METHODS))
484 sds_converted = sds_and_msds_to_sds(sds)
486 settings: Dict[str, Any] = dict(kwargs)
487 settings.update({"show": False})
489 specifications = [colour_quality_scale(sd, True, method) for sd in sds_converted]
491 _figure, axes = plot_colour_quality_bars(specifications, **settings)
493 title = (
494 f"Colour Quality Scale - {', '.join([sd.display_name for sd in sds_converted])}"
495 )
497 settings = {"axes": axes, "title": title}
498 settings.update(kwargs)
500 return render(**settings)