Coverage for plotting/colorimetry.py: 79%
206 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"""
2Colorimetry Plotting
3====================
5Define the colorimetry plotting objects:
7- :func:`colour.plotting.plot_single_sd`
8- :func:`colour.plotting.plot_multi_sds`
9- :func:`colour.plotting.plot_single_cmfs`
10- :func:`colour.plotting.plot_multi_cmfs`
11- :func:`colour.plotting.plot_single_illuminant_sd`
12- :func:`colour.plotting.plot_multi_illuminant_sds`
13- :func:`colour.plotting.plot_visible_spectrum`
14- :func:`colour.plotting.plot_single_lightness_function`
15- :func:`colour.plotting.plot_multi_lightness_functions`
16- :func:`colour.plotting.plot_single_luminance_function`
17- :func:`colour.plotting.plot_multi_luminance_functions`
18- :func:`colour.plotting.plot_blackbody_spectral_radiance`
19- :func:`colour.plotting.plot_blackbody_colours`
21References
22----------
23- :cite:`Spiker2015a` : Borer, T. (2017). Private Discussion with Mansencal,
24 T. and Shaw, N.
25"""
27from __future__ import annotations
29import typing
30from functools import reduce
32import matplotlib.pyplot as plt
33import numpy as np
35if typing.TYPE_CHECKING:
36 from collections.abc import ValuesView
38from matplotlib.patches import Polygon
40if typing.TYPE_CHECKING:
41 from matplotlib.figure import Figure
42 from matplotlib.axes import Axes
44from colour.algebra import LinearInterpolator, normalise_maximum, sdiv, sdiv_mode
45from colour.colorimetry import (
46 CCS_ILLUMINANTS,
47 LIGHTNESS_METHODS,
48 LUMINANCE_METHODS,
49 SDS_ILLUMINANTS,
50 MultiSpectralDistributions,
51 SpectralDistribution,
52 SpectralShape,
53 sd_blackbody,
54 sd_ones,
55 sd_to_XYZ,
56 sds_and_msds_to_sds,
57 wavelength_to_XYZ,
58)
60if typing.TYPE_CHECKING:
61 from colour.hints import (
62 Any,
63 Callable,
64 Dict,
65 Sequence,
66 Tuple,
67 )
69from colour.hints import List, cast
70from colour.plotting import (
71 CONSTANTS_COLOUR_STYLE,
72 XYZ_to_plotting_colourspace,
73 artist,
74 filter_cmfs,
75 filter_illuminants,
76 filter_passthrough,
77 override_style,
78 plot_multi_functions,
79 plot_single_colour_swatch,
80 render,
81 update_settings_collection,
82)
83from colour.utilities import (
84 as_float_array,
85 domain_range_scale,
86 first_item,
87 ones,
88 optional,
89 tstack,
90)
92__author__ = "Colour Developers"
93__copyright__ = "Copyright 2013 Colour Developers"
94__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
95__maintainer__ = "Colour Developers"
96__email__ = "colour-developers@colour-science.org"
97__status__ = "Production"
99__all__ = [
100 "plot_single_sd",
101 "plot_multi_sds",
102 "plot_single_cmfs",
103 "plot_multi_cmfs",
104 "plot_single_illuminant_sd",
105 "plot_multi_illuminant_sds",
106 "plot_visible_spectrum",
107 "plot_single_lightness_function",
108 "plot_multi_lightness_functions",
109 "plot_single_luminance_function",
110 "plot_multi_luminance_functions",
111 "plot_blackbody_spectral_radiance",
112 "plot_blackbody_colours",
113]
116@override_style()
117def plot_single_sd(
118 sd: SpectralDistribution,
119 cmfs: (
120 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
121 ) = "CIE 1931 2 Degree Standard Observer",
122 out_of_gamut_clipping: bool = True,
123 modulate_colours_with_sd_amplitude: bool = False,
124 equalize_sd_amplitude: bool = False,
125 **kwargs: Any,
126) -> Tuple[Figure, Axes]:
127 """
128 Plot the specified spectral distribution.
130 Parameters
131 ----------
132 sd
133 Spectral distribution to plot.
134 cmfs
135 Standard observer colour matching functions used for computing the
136 spectrum domain and colours. ``cmfs`` can be of any type or form
137 supported by the :func:`colour.plotting.common.filter_cmfs` definition.
138 out_of_gamut_clipping
139 Whether to clip out of gamut colours. Otherwise, the colours will
140 be offset by the absolute minimal colour, resulting in rendering
141 on a gray background that is less saturated and smoother.
142 modulate_colours_with_sd_amplitude
143 Whether to modulate the colours with the spectral distribution
144 amplitude.
145 equalize_sd_amplitude
146 Whether to equalize the spectral distribution amplitude.
147 Equalization occurs after the colours modulation; thus, setting
148 both arguments to *True* will generate a spectrum strip where each
149 wavelength colour is modulated by the spectral distribution
150 amplitude. The usual 5% margin above the spectral distribution is
151 also omitted.
153 Other Parameters
154 ----------------
155 kwargs
156 {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
157 See the documentation of the previously listed definitions.
159 Returns
160 -------
161 :class:`tuple`
162 Current figure and axes.
164 References
165 ----------
166 :cite:`Spiker2015a`
168 Examples
169 --------
170 >>> from colour import SpectralDistribution
171 >>> data = {
172 ... 500: 0.0651,
173 ... 520: 0.0705,
174 ... 540: 0.0772,
175 ... 560: 0.0870,
176 ... 580: 0.1128,
177 ... 600: 0.1360,
178 ... }
179 >>> sd = SpectralDistribution(data, name="Custom")
180 >>> plot_single_sd(sd) # doctest: +ELLIPSIS
181 (<Figure size ... with 1 Axes>, <...Axes...>)
183 .. image:: ../_static/Plotting_Plot_Single_SD.png
184 :align: center
185 :alt: plot_single_sd
186 """
188 _figure, axes = artist(**kwargs)
190 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))
192 sd = sd.copy()
193 sd.interpolator = LinearInterpolator
194 wavelengths = cmfs.wavelengths[
195 np.logical_and(
196 cmfs.wavelengths >= max(min(cmfs.wavelengths), min(sd.wavelengths)),
197 cmfs.wavelengths <= min(max(cmfs.wavelengths), max(sd.wavelengths)),
198 )
199 ]
200 values = sd[wavelengths]
202 RGB = XYZ_to_plotting_colourspace(
203 wavelength_to_XYZ(wavelengths, cmfs),
204 CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["E"],
205 apply_cctf_encoding=False,
206 )
208 if not out_of_gamut_clipping:
209 RGB += np.abs(np.min(RGB))
211 RGB = normalise_maximum(RGB)
213 if modulate_colours_with_sd_amplitude:
214 with sdiv_mode():
215 RGB *= sdiv(values, np.max(values))[..., None]
217 RGB = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding(RGB)
219 if equalize_sd_amplitude:
220 values = ones(values.shape)
222 margin = 0 if equalize_sd_amplitude else 0.05
224 x_min, x_max = min(wavelengths), max(wavelengths)
225 y_min, y_max = 0, max(values) + max(values) * margin
227 polygon = Polygon(
228 np.vstack(
229 [
230 (x_min, 0),
231 tstack([wavelengths, values]),
232 (x_max, 0),
233 ]
234 ),
235 facecolor="none",
236 edgecolor="none",
237 zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
238 )
239 axes.add_patch(polygon)
241 axes.bar(
242 x=wavelengths,
243 height=max(values),
244 width=np.min(np.diff(wavelengths)) if len(wavelengths) > 1 else 1,
245 color=RGB,
246 align="edge",
247 clip_path=polygon,
248 zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
249 )
251 axes.plot(
252 wavelengths,
253 values,
254 color=CONSTANTS_COLOUR_STYLE.colour.dark,
255 zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
256 )
258 settings: Dict[str, Any] = {
259 "axes": axes,
260 "bounding_box": (x_min, x_max, y_min, y_max),
261 "title": f"{sd.display_name} - {cmfs.display_name}",
262 "x_label": "Wavelength $\\lambda$ (nm)",
263 "y_label": "Spectral Distribution",
264 }
265 settings.update(kwargs)
267 return render(**settings)
270@override_style()
271def plot_multi_sds(
272 sds: (
273 Sequence[SpectralDistribution | MultiSpectralDistributions]
274 | SpectralDistribution
275 | MultiSpectralDistributions
276 | ValuesView
277 ),
278 plot_kwargs: dict | List[dict] | None = None,
279 **kwargs: Any,
280) -> Tuple[Figure, Axes]:
281 """
282 Plot specified spectral distributions.
284 Parameters
285 ----------
286 sds
287 Spectral distributions or multi-spectral distributions to plot.
288 ``sds`` can be a single
289 :class:`colour.MultiSpectralDistributions` class instance, a list
290 of :class:`colour.MultiSpectralDistributions` class instances or
291 a list of :class:`colour.SpectralDistribution` class instances.
292 plot_kwargs
293 Keyword arguments for the :func:`matplotlib.pyplot.plot`
294 definition, used to control the style of the plotted spectral
295 distributions. ``plot_kwargs`` can be either a single dictionary
296 applied to all the plotted spectral distributions with the same
297 settings or a sequence of dictionaries with different settings
298 for each plotted spectral distribution. The following special
299 keyword arguments can also be used:
301 - ``illuminant`` : The illuminant used to compute the spectral
302 distributions colours. The default is the illuminant
303 associated with the whitepoint of the default plotting
304 colourspace. ``illuminant`` can be of any type or form
305 supported by the :func:`colour.plotting.common.filter_cmfs`
306 definition.
307 - ``cmfs`` : The standard observer colour matching functions
308 used for computing the spectral distributions colours.
309 ``cmfs`` can be of any type or form supported by the
310 :func:`colour.plotting.common.filter_cmfs` definition.
311 - ``normalise_sd_colours`` : Whether to normalise the computed
312 spectral distributions colours. The default is *True*.
313 - ``use_sd_colours`` : Whether to use the computed spectral
314 distributions colours under the plotting colourspace
315 illuminant. Alternatively, it is possible to use the
316 :func:`matplotlib.pyplot.plot` definition ``color`` argument
317 with pre-computed values. The default is *True*.
319 Other Parameters
320 ----------------
321 kwargs
322 {:func:`colour.plotting.artist`,
323 :func:`colour.plotting.render`},
324 See the documentation of the previously listed definitions.
326 Returns
327 -------
328 :class:`tuple`
329 Current figure and axes.
331 Examples
332 --------
333 >>> from colour import SpectralDistribution
334 >>> data_1 = {
335 ... 500: 0.004900,
336 ... 510: 0.009300,
337 ... 520: 0.063270,
338 ... 530: 0.165500,
339 ... 540: 0.290400,
340 ... 550: 0.433450,
341 ... 560: 0.594500,
342 ... }
343 >>> data_2 = {
344 ... 500: 0.323000,
345 ... 510: 0.503000,
346 ... 520: 0.710000,
347 ... 530: 0.862000,
348 ... 540: 0.954000,
349 ... 550: 0.994950,
350 ... 560: 0.995000,
351 ... }
352 >>> sd_1 = SpectralDistribution(data_1, name="Custom 1")
353 >>> sd_2 = SpectralDistribution(data_2, name="Custom 2")
354 >>> plot_kwargs = [
355 ... {"use_sd_colours": True},
356 ... {"use_sd_colours": True, "linestyle": "dashed"},
357 ... ]
358 >>> plot_multi_sds([sd_1, sd_2], plot_kwargs=plot_kwargs)
359 ... # doctest: +ELLIPSIS
360 (<Figure size ... with 1 Axes>, <...Axes...>)
362 .. image:: ../_static/Plotting_Plot_Multi_SDS.png
363 :align: center
364 :alt: plot_multi_sds
365 """
367 _figure, axes = artist(**kwargs)
369 sds_converted = sds_and_msds_to_sds(sds)
371 plot_settings_collection = [
372 {
373 "label": f"{sd.display_name}",
374 "zorder": CONSTANTS_COLOUR_STYLE.zorder.midground_line,
375 "cmfs": "CIE 1931 2 Degree Standard Observer",
376 "illuminant": SDS_ILLUMINANTS["E"],
377 "use_sd_colours": False,
378 "normalise_sd_colours": False,
379 }
380 for sd in sds_converted
381 ]
383 if plot_kwargs is not None:
384 update_settings_collection(
385 plot_settings_collection, plot_kwargs, len(sds_converted)
386 )
388 x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], []
389 for i, sd in enumerate(sds_converted):
390 plot_settings = plot_settings_collection[i]
392 cmfs = cast(
393 "MultiSpectralDistributions",
394 first_item(filter_cmfs(plot_settings.pop("cmfs")).values()),
395 )
396 illuminant = cast(
397 "SpectralDistribution",
398 first_item(filter_illuminants(plot_settings.pop("illuminant")).values()),
399 )
400 normalise_sd_colours = plot_settings.pop("normalise_sd_colours")
401 use_sd_colours = plot_settings.pop("use_sd_colours")
403 wavelengths, values = sd.wavelengths, sd.values
405 shape = sd.shape
406 x_limit_min.append(shape.start)
407 x_limit_max.append(shape.end)
408 y_limit_min.append(min(values))
409 y_limit_max.append(max(values))
411 if use_sd_colours:
412 with domain_range_scale("1"):
413 XYZ = sd_to_XYZ(sd, cmfs, illuminant)
415 if normalise_sd_colours:
416 XYZ /= XYZ[..., 1]
418 plot_settings["color"] = np.clip(XYZ_to_plotting_colourspace(XYZ), 0, 1)
420 axes.plot(wavelengths, values, **plot_settings)
422 bounding_box = (
423 min(x_limit_min),
424 max(x_limit_max),
425 min(y_limit_min),
426 max(y_limit_max) * 1.05,
427 )
428 settings: Dict[str, Any] = {
429 "axes": axes,
430 "bounding_box": bounding_box,
431 "legend": True,
432 "x_label": "Wavelength $\\lambda$ (nm)",
433 "y_label": "Spectral Distribution",
434 }
435 settings.update(kwargs)
437 return render(**settings)
440@override_style()
441def plot_single_cmfs(
442 cmfs: (
443 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
444 ) = "CIE 1931 2 Degree Standard Observer",
445 **kwargs: Any,
446) -> Tuple[Figure, Axes]:
447 """
448 Plot specified colour matching functions.
450 Parameters
451 ----------
452 cmfs
453 Colour matching functions to plot. ``cmfs`` can be of any type or form
454 supported by the :func:`colour.plotting.common.filter_cmfs` definition.
456 Other Parameters
457 ----------------
458 kwargs
459 {:func:`colour.plotting.artist`,
460 :func:`colour.plotting.plot_multi_cmfs`,
461 :func:`colour.plotting.render`},
462 See the documentation of the previously listed definitions.
464 Returns
465 -------
466 :class:`tuple`
467 Current figure and axes.
469 Examples
470 --------
471 >>> plot_single_cmfs("CIE 1931 2 Degree Standard Observer")
472 ... # doctest: +ELLIPSIS
473 (<Figure size ... with 1 Axes>, <...Axes...>)
475 .. image:: ../_static/Plotting_Plot_Single_CMFS.png
476 :align: center
477 :alt: plot_single_cmfs
478 """
480 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))
482 settings: Dict[str, Any] = {
483 "title": f"{cmfs.display_name} - Colour Matching Functions"
484 }
485 settings.update(kwargs)
487 return plot_multi_cmfs((cmfs,), **settings)
490@override_style()
491def plot_multi_cmfs(
492 cmfs: (
493 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
494 ),
495 **kwargs: Any,
496) -> Tuple[Figure, Axes]:
497 """
498 Plot the specified colour matching functions.
500 Parameters
501 ----------
502 cmfs
503 Colour matching functions to plot. ``cmfs`` elements can be of any
504 type or form supported by the
505 :func:`colour.plotting.common.filter_cmfs` definition.
507 Other Parameters
508 ----------------
509 kwargs
510 {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
511 See the documentation of the previously listed definitions.
513 Returns
514 -------
515 :class:`tuple`
516 Current figure and axes.
518 Examples
519 --------
520 >>> cmfs = [
521 ... "CIE 1931 2 Degree Standard Observer",
522 ... "CIE 1964 10 Degree Standard Observer",
523 ... ]
524 >>> plot_multi_cmfs(cmfs) # doctest: +ELLIPSIS
525 (<Figure size ... with 1 Axes>, <...Axes...>)
527 .. image:: ../_static/Plotting_Plot_Multi_CMFS.png
528 :align: center
529 :alt: plot_multi_cmfs
530 """
532 cmfs = cast("List[MultiSpectralDistributions]", list(filter_cmfs(cmfs).values())) # pyright: ignore
534 _figure, axes = artist(**kwargs)
536 axes.axhline(
537 color=CONSTANTS_COLOUR_STYLE.colour.dark,
538 linestyle="--",
539 zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line,
540 )
542 x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], []
543 for i, cmfs_i in enumerate(cmfs):
544 for j, RGB in enumerate(as_float_array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])):
545 RGB = [ # noqa: PLW2901
546 reduce(lambda y, _: y * 0.5, range(i), x) for x in RGB
547 ]
548 values = cmfs_i.values[:, j]
550 shape = cmfs_i.shape
551 x_limit_min.append(shape.start)
552 x_limit_max.append(shape.end)
553 y_limit_min.append(np.min(values))
554 y_limit_max.append(np.max(values))
556 axes.plot(
557 cmfs_i.wavelengths,
558 values,
559 color=RGB,
560 label=f"{cmfs_i.display_labels[j]} - {cmfs_i.display_name}",
561 zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
562 )
564 bounding_box = (
565 min(x_limit_min),
566 max(x_limit_max),
567 min(y_limit_min) - np.abs(np.min(y_limit_min)) * 0.05,
568 max(y_limit_max) + np.abs(np.max(y_limit_max)) * 0.05,
569 )
570 cmfs_display_names = ", ".join([cmfs_i.display_name for cmfs_i in cmfs])
571 title = f"{cmfs_display_names} - Colour Matching Functions"
573 settings: Dict[str, Any] = {
574 "axes": axes,
575 "bounding_box": bounding_box,
576 "legend": True,
577 "title": title,
578 "x_label": "Wavelength $\\lambda$ (nm)",
579 "y_label": "Tristimulus Values",
580 }
581 settings.update(kwargs)
583 return render(**settings)
586@override_style()
587def plot_single_illuminant_sd(
588 illuminant: SpectralDistribution | str,
589 cmfs: (
590 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
591 ) = "CIE 1931 2 Degree Standard Observer",
592 **kwargs: Any,
593) -> Tuple[Figure, Axes]:
594 """
595 Plot the specified single illuminant spectral distribution.
597 Parameters
598 ----------
599 illuminant
600 Illuminant to plot. ``illuminant`` can be of any type or form
601 supported by the :func:`colour.plotting.common.filter_illuminants`
602 definition.
603 cmfs
604 Standard observer colour matching functions used for computing the
605 spectrum domain and colours. ``cmfs`` can be of any type or form
606 supported by the :func:`colour.plotting.common.filter_cmfs` definition.
608 Other Parameters
609 ----------------
610 kwargs
611 {:func:`colour.plotting.artist`,
612 :func:`colour.plotting.plot_single_sd`,
613 :func:`colour.plotting.render`},
614 See the documentation of the previously listed definitions.
616 Returns
617 -------
618 :class:`tuple`
619 Current figure and axes.
621 References
622 ----------
623 :cite:`Spiker2015a`
625 Examples
626 --------
627 >>> plot_single_illuminant_sd("A") # doctest: +ELLIPSIS
628 (<Figure size ... with 1 Axes>, <...Axes...>)
630 .. image:: ../_static/Plotting_Plot_Single_Illuminant_SD.png
631 :align: center
632 :alt: plot_single_illuminant_sd
633 """
635 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))
637 title = f"Illuminant {illuminant} - {cmfs.display_name}"
639 illuminant = cast(
640 "SpectralDistribution",
641 first_item(filter_illuminants(illuminant).values()),
642 )
644 settings: Dict[str, Any] = {"title": title, "y_label": "Relative Power"}
645 settings.update(kwargs)
647 return plot_single_sd(illuminant, **settings)
650@override_style()
651def plot_multi_illuminant_sds(
652 illuminants: (SpectralDistribution | str | Sequence[SpectralDistribution | str]),
653 **kwargs: Any,
654) -> Tuple[Figure, Axes]:
655 """
656 Plot the spectral distributions of the specified illuminants.
658 Parameters
659 ----------
660 illuminants
661 Illuminants to plot. ``illuminants`` elements can be of any type
662 or form supported by the
663 :func:`colour.plotting.common.filter_illuminants` definition.
665 Other Parameters
666 ----------------
667 kwargs
668 {:func:`colour.plotting.artist`,
669 :func:`colour.plotting.plot_multi_sds`,
670 :func:`colour.plotting.render`},
671 See the documentation of the previously listed definitions.
673 Returns
674 -------
675 :class:`tuple`
676 Current figure and axes.
678 Examples
679 --------
680 >>> plot_multi_illuminant_sds(["A", "B", "C"]) # doctest: +ELLIPSIS
681 (<Figure size ... with 1 Axes>, <...Axes...>)
683 .. image:: ../_static/Plotting_Plot_Multi_Illuminant_SDS.png
684 :align: center
685 :alt: plot_multi_illuminant_sds
686 """
688 if "plot_kwargs" not in kwargs:
689 kwargs["plot_kwargs"] = {}
691 SD_E = SDS_ILLUMINANTS["E"]
692 if isinstance(kwargs["plot_kwargs"], dict):
693 kwargs["plot_kwargs"]["illuminant"] = SD_E
694 else:
695 for i in range(len(kwargs["plot_kwargs"])):
696 kwargs["plot_kwargs"][i]["illuminant"] = SD_E
698 illuminants = cast(
699 "List[SpectralDistribution]",
700 list(filter_illuminants(illuminants).values()),
701 ) # pyright: ignore
703 illuminant_display_names = ", ".join(
704 [illuminant.display_name for illuminant in illuminants]
705 )
706 title = f"{illuminant_display_names} - Illuminants Spectral Distributions"
708 settings: Dict[str, Any] = {"title": title, "y_label": "Relative Power"}
709 settings.update(kwargs)
711 return plot_multi_sds(illuminants, **settings)
714@override_style(
715 **{
716 "ytick.left": False,
717 "ytick.labelleft": False,
718 }
719)
720def plot_visible_spectrum(
721 cmfs: (
722 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
723 ) = "CIE 1931 2 Degree Standard Observer",
724 out_of_gamut_clipping: bool = True,
725 **kwargs: Any,
726) -> Tuple[Figure, Axes]:
727 """
728 Plot the visible colour spectrum using the specified standard observer
729 *CIE XYZ* colour matching functions.
731 Parameters
732 ----------
733 cmfs
734 Standard observer colour matching functions used for computing the
735 spectrum domain and colours. ``cmfs`` can be of any type or form
736 supported by the :func:`colour.plotting.common.filter_cmfs` definition.
737 out_of_gamut_clipping
738 Whether to clip out of gamut colours. Otherwise, the colours will
739 be offset by the absolute minimal colour, resulting in rendering
740 on a gray background that is less saturated and smoother.
742 Other Parameters
743 ----------------
744 kwargs
745 {:func:`colour.plotting.artist`,
746 :func:`colour.plotting.plot_single_sd`,
747 :func:`colour.plotting.render`},
748 See the documentation of the previously listed definitions.
750 Returns
751 -------
752 :class:`tuple`
753 Current figure and axes.
755 References
756 ----------
757 :cite:`Spiker2015a`
759 Examples
760 --------
761 >>> plot_visible_spectrum() # doctest: +ELLIPSIS
762 (<Figure size ... with 1 Axes>, <...Axes...>)
764 .. image:: ../_static/Plotting_Plot_Visible_Spectrum.png
765 :align: center
766 :alt: plot_visible_spectrum
767 """
769 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))
771 bounding_box = (min(cmfs.wavelengths), max(cmfs.wavelengths), 0, 1)
773 settings: Dict[str, Any] = {"bounding_box": bounding_box, "y_label": None}
774 settings.update(kwargs)
775 settings["show"] = False
777 _figure, axes = plot_single_sd(
778 sd_ones(cmfs.shape),
779 cmfs=cmfs,
780 out_of_gamut_clipping=out_of_gamut_clipping,
781 **settings,
782 )
784 # Removing wavelength line as it doubles with the axes spine.
785 axes.lines[0].remove()
787 settings = {
788 "axes": axes,
789 "show": True,
790 "title": f"The Visible Spectrum - {cmfs.display_name}",
791 "x_label": "Wavelength $\\lambda$ (nm)",
792 }
793 settings.update(kwargs)
795 return render(**settings)
798@override_style()
799def plot_single_lightness_function(
800 function: Callable | str, **kwargs: Any
801) -> Tuple[Figure, Axes]:
802 """
803 Plot the specified *Lightness* function.
805 Parameters
806 ----------
807 function
808 *Lightness* function to plot. ``function`` can be of any type or
809 form supported by the
810 :func:`colour.plotting.common.filter_passthrough` definition.
812 Other Parameters
813 ----------------
814 kwargs
815 {:func:`colour.plotting.artist`,
816 :func:`colour.plotting.plot_multi_functions`,
817 :func:`colour.plotting.render`},
818 See the documentation of the previously listed definitions.
820 Returns
821 -------
822 :class:`tuple`
823 Current figure and axes.
825 Examples
826 --------
827 >>> plot_single_lightness_function("CIE 1976") # doctest: +ELLIPSIS
828 (<Figure size ... with 1 Axes>, <...Axes...>)
830 .. image:: ../_static/Plotting_Plot_Single_Lightness_Function.png
831 :align: center
832 :alt: plot_single_lightness_function
833 """
835 settings: Dict[str, Any] = {"title": f"{function} - Lightness Function"}
836 settings.update(kwargs)
838 return plot_multi_lightness_functions((function,), **settings)
841@override_style()
842def plot_multi_lightness_functions(
843 functions: Callable | str | Sequence[Callable | str],
844 **kwargs: Any,
845) -> Tuple[Figure, Axes]:
846 """
847 Plot the specified *Lightness* functions.
849 Parameters
850 ----------
851 functions
852 *Lightness* functions to plot. ``functions`` elements can be of any
853 type or form supported by the
854 :func:`colour.plotting.common.filter_passthrough` definition.
856 Other Parameters
857 ----------------
858 kwargs
859 {:func:`colour.plotting.artist`,
860 :func:`colour.plotting.plot_multi_functions`,
861 :func:`colour.plotting.render`},
862 See the documentation of the previously listed definitions.
864 Returns
865 -------
866 :class:`tuple`
867 Current figure and axes.
869 Examples
870 --------
871 >>> plot_multi_lightness_functions(["CIE 1976", "Wyszecki 1963"])
872 ... # doctest: +ELLIPSIS
873 (<Figure size ... with 1 Axes>, <...Axes...>)
875 .. image:: ../_static/Plotting_Plot_Multi_Lightness_Functions.png
876 :align: center
877 :alt: plot_multi_lightness_functions
878 """
880 functions_filtered = filter_passthrough(LIGHTNESS_METHODS, functions)
882 settings: Dict[str, Any] = {
883 "bounding_box": (0, 1, 0, 1),
884 "legend": True,
885 "title": f"{', '.join(functions_filtered)} - Lightness Functions",
886 "x_label": "Normalised Relative Luminance Y",
887 "y_label": "Normalised Lightness",
888 }
889 settings.update(kwargs)
891 with domain_range_scale("1"):
892 return plot_multi_functions(functions_filtered, **settings)
895@override_style()
896def plot_single_luminance_function(
897 function: Callable | str, **kwargs: Any
898) -> Tuple[Figure, Axes]:
899 """
900 Plot the specified *Luminance* function.
902 Parameters
903 ----------
904 function
905 *Luminance* function to plot.
907 Other Parameters
908 ----------------
909 kwargs
910 {:func:`colour.plotting.artist`,
911 :func:`colour.plotting.plot_multi_functions`,
912 :func:`colour.plotting.render`},
913 See the documentation of the previously listed definitions.
915 Returns
916 -------
917 :class:`tuple`
918 Current figure and axes.
920 Examples
921 --------
922 >>> plot_single_luminance_function("CIE 1976") # doctest: +ELLIPSIS
923 (<Figure size ... with 1 Axes>, <...Axes...>)
925 .. image:: ../_static/Plotting_Plot_Single_Luminance_Function.png
926 :align: center
927 :alt: plot_single_luminance_function
928 """
930 settings: Dict[str, Any] = {"title": f"{function} - Luminance Function"}
931 settings.update(kwargs)
933 return plot_multi_luminance_functions((function,), **settings)
936@override_style()
937def plot_multi_luminance_functions(
938 functions: Callable | str | Sequence[Callable | str],
939 **kwargs: Any,
940) -> Tuple[Figure, Axes]:
941 """
942 Plot the specified *Luminance* functions.
944 Parameters
945 ----------
946 functions
947 *Luminance* functions to plot. ``functions`` elements can be of any
948 type or form supported by the
949 :func:`colour.plotting.common.filter_passthrough` definition.
951 Other Parameters
952 ----------------
953 kwargs
954 {:func:`colour.plotting.artist`,
955 :func:`colour.plotting.plot_multi_functions`,
956 :func:`colour.plotting.render`},
957 See the documentation of the previously listed definitions.
959 Returns
960 -------
961 :class:`tuple`
962 Current figure and axes.
964 Examples
965 --------
966 >>> plot_multi_luminance_functions(["CIE 1976", "Newhall 1943"])
967 ... # doctest: +ELLIPSIS
968 (<Figure size ... with 1 Axes>, <...Axes...>)
970 .. image:: ../_static/Plotting_Plot_Multi_Luminance_Functions.png
971 :align: center
972 :alt: plot_multi_luminance_functions
973 """
975 functions_filtered = filter_passthrough(LUMINANCE_METHODS, functions)
977 settings: Dict[str, Any] = {
978 "bounding_box": (0, 1, 0, 1),
979 "legend": True,
980 "title": f"{', '.join(functions_filtered)} - Luminance Functions",
981 "x_label": "Normalised Munsell Value / Lightness",
982 "y_label": "Normalised Relative Luminance Y",
983 }
984 settings.update(kwargs)
986 with domain_range_scale("1"):
987 return plot_multi_functions(functions_filtered, **settings)
990@override_style()
991def plot_blackbody_spectral_radiance(
992 temperature: float = 3500,
993 cmfs: (
994 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
995 ) = "CIE 1931 2 Degree Standard Observer",
996 blackbody: str = "VY Canis Major",
997 **kwargs: Any,
998) -> Tuple[Figure, Axes]:
999 """
1000 Plot the spectral radiance of a blackbody at the specified temperature.
1002 Parameters
1003 ----------
1004 temperature
1005 Blackbody temperature.
1006 cmfs
1007 Standard observer colour matching functions used for computing the
1008 spectrum domain and colours. ``cmfs`` can be of any type or form
1009 supported by the :func:`colour.plotting.common.filter_cmfs` definition.
1010 blackbody
1011 Blackbody name.
1013 Other Parameters
1014 ----------------
1015 kwargs
1016 {:func:`colour.plotting.artist`,
1017 :func:`colour.plotting.plot_single_sd`,
1018 :func:`colour.plotting.render`},
1019 See the documentation of the previously listed definitions.
1021 Returns
1022 -------
1023 :class:`tuple`
1024 Current figure and axes.
1026 Examples
1027 --------
1028 >>> plot_blackbody_spectral_radiance(3500, blackbody="VY Canis Major")
1029 ... # doctest: +ELLIPSIS
1030 (<Figure size ... with 2 Axes>, <...Axes...>)
1032 .. image:: ../_static/Plotting_Plot_Blackbody_Spectral_Radiance.png
1033 :align: center
1034 :alt: plot_blackbody_spectral_radiance
1035 """
1037 figure = plt.figure()
1039 figure.subplots_adjust(hspace=CONSTANTS_COLOUR_STYLE.geometry.short / 2)
1041 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))
1043 sd = sd_blackbody(temperature, cmfs.shape)
1045 axes = figure.add_subplot(211)
1046 settings: Dict[str, Any] = {
1047 "axes": axes,
1048 "title": f"{blackbody} - Spectral Radiance",
1049 "y_label": "W / (sr m$^2$) / m",
1050 }
1051 settings.update(kwargs)
1052 settings["show"] = False
1054 plot_single_sd(sd, cmfs.name, **settings)
1056 axes = figure.add_subplot(212)
1058 with domain_range_scale("1"):
1059 XYZ = sd_to_XYZ(sd, cmfs)
1061 RGB = normalise_maximum(XYZ_to_plotting_colourspace(XYZ))
1063 settings = {
1064 "axes": axes,
1065 "aspect": None,
1066 "title": f"{blackbody} - Colour",
1067 "x_label": f"{temperature}K",
1068 "y_label": "",
1069 "x_ticker": False,
1070 "y_ticker": False,
1071 }
1072 settings.update(kwargs)
1073 settings["show"] = False
1075 figure, axes = plot_single_colour_swatch(RGB, **settings)
1077 settings = {"axes": axes, "show": True}
1078 settings.update(kwargs)
1080 return render(**settings)
1083@override_style(
1084 **{
1085 "ytick.left": False,
1086 "ytick.labelleft": False,
1087 }
1088)
1089def plot_blackbody_colours(
1090 shape: SpectralShape | None = None,
1091 cmfs: (
1092 MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
1093 ) = "CIE 1931 2 Degree Standard Observer",
1094 **kwargs: Any,
1095) -> Tuple[Figure, Axes]:
1096 """
1097 Plot blackbody colours across a temperature range.
1099 Parameters
1100 ----------
1101 shape
1102 Spectral shape defining the temperature range and sampling interval
1103 for the plot boundaries.
1104 cmfs
1105 Standard observer colour matching functions used for computing the
1106 spectrum domain and colours. ``cmfs`` can be of any type or form
1107 supported by the :func:`colour.plotting.common.filter_cmfs` definition.
1109 Other Parameters
1110 ----------------
1111 kwargs
1112 {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
1113 See the documentation of the previously listed definitions.
1115 Returns
1116 -------
1117 :class:`tuple`
1118 Current figure and axes.
1120 Examples
1121 --------
1122 >>> plot_blackbody_colours(SpectralShape(150, 12500, 50))
1123 ... # doctest: +ELLIPSIS
1124 (<Figure size ... with 1 Axes>, <...Axes...>)
1126 .. image:: ../_static/Plotting_Plot_Blackbody_Colours.png
1127 :align: center
1128 :alt: plot_blackbody_colours
1129 """
1131 shape = optional(shape, SpectralShape(150, 12500, 50))
1133 _figure, axes = artist(**kwargs)
1135 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))
1137 RGB = []
1138 temperatures = []
1140 for temperature in shape:
1141 sd = sd_blackbody(temperature, cmfs.shape)
1143 with domain_range_scale("1"):
1144 XYZ = sd_to_XYZ(sd, cmfs)
1146 RGB.append(normalise_maximum(XYZ_to_plotting_colourspace(XYZ)))
1147 temperatures.append(temperature)
1149 x_min, x_max = min(temperatures), max(temperatures)
1150 y_min, y_max = 0, 1
1152 padding = 0.1
1153 axes.bar(
1154 x=as_float_array(temperatures) - padding,
1155 height=1,
1156 width=shape.interval + (padding * shape.interval),
1157 color=RGB,
1158 align="edge",
1159 zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
1160 )
1162 settings: Dict[str, Any] = {
1163 "axes": axes,
1164 "bounding_box": (x_min, x_max, y_min, y_max),
1165 "title": "Blackbody Colours",
1166 "x_label": "Temperature K",
1167 "y_label": None,
1168 }
1169 settings.update(kwargs)
1171 return render(**settings)