Coverage for plotting/colorimetry.py: 79%

1"""

2 Colorimetry Plotting

3====================

4

5 Define the colorimetry plotting objects:

6

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`

20

21 References

22----------

23- :cite:`Spiker2015a` : Borer, T. (2017). Private Discussion with Mansencal,

24 T. and Shaw, N.

25"""

26

27 from __future__ import annotations

28

29 import typing

30 from functools import reduce

31

32 import matplotlib.pyplot as plt

33 import numpy as np

34

35 if typing.TYPE_CHECKING:

36 from collections.abc import ValuesView

37

38 from matplotlib.patches import Polygon

39

40 if typing.TYPE_CHECKING:

41 from matplotlib.figure import Figure

42 from matplotlib.axes import Axes

43

44 from colour.algebra import LinearInterpolator, normalise_maximum, sdiv, sdiv_mode

45 from 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)

59

60 if typing.TYPE_CHECKING:

61 from colour.hints import (

62 Any,

63 Callable,

64 Dict,

65 Sequence,

66 Tuple,

67 )

68

69 from colour.hints import List, cast

70 from 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)

83 from colour.utilities import (

84 as_float_array,

85 domain_range_scale,

86 first_item,

87 ones,

88 optional,

89 tstack,

90)

91

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"

98

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]

114

115

116@override_style()

117 def 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.

129

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.

152

153 Other Parameters

154 ----------------

155 kwargs

156 {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},

157 See the documentation of the previously listed definitions.

158

159 Returns

160 -------

161 :class:`tuple`

162 Current figure and axes.

163

164 References

165 ----------

166 :cite:`Spiker2015a`

167

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...>)

182

183 .. image:: ../_static/Plotting_Plot_Single_SD.png

184 :align: center

185 :alt: plot_single_sd

186 """

187

188 _figure, axes = artist(**kwargs)

189

190 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

191

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]

201

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 )

207

208 if not out_of_gamut_clipping:

209 RGB += np.abs(np.min(RGB))

210

211 RGB = normalise_maximum(RGB)

212

213 if modulate_colours_with_sd_amplitude:

214 with sdiv_mode():

215 RGB *= sdiv(values, np.max(values))[..., None]

216

217 RGB = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding(RGB)

218

219 if equalize_sd_amplitude:

220 values = ones(values.shape)

221

222 margin = 0 if equalize_sd_amplitude else 0.05

223

224 x_min, x_max = min(wavelengths), max(wavelengths)

225 y_min, y_max = 0, max(values) + max(values) * margin

226

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)

240

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 )

250

251 axes.plot(

252 wavelengths,

253 values,

254 color=CONSTANTS_COLOUR_STYLE.colour.dark,

255 zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,

256 )

257

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)

266

267 return render(**settings)

268

269

270@override_style()

271 def 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.

283

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:

300

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*.

318

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.

325

326 Returns

327 -------

328 :class:`tuple`

329 Current figure and axes.

330

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...>)

361

362 .. image:: ../_static/Plotting_Plot_Multi_SDS.png

363 :align: center

364 :alt: plot_multi_sds

365 """

366

367 _figure, axes = artist(**kwargs)

368

369 sds_converted = sds_and_msds_to_sds(sds)

370

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 ]

382

383 if plot_kwargs is not None:

384 update_settings_collection(

385 plot_settings_collection, plot_kwargs, len(sds_converted)

386 )

387

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]

391

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")

402

403 wavelengths, values = sd.wavelengths, sd.values

404

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))

410

411 if use_sd_colours:

412 with domain_range_scale("1"):

413 XYZ = sd_to_XYZ(sd, cmfs, illuminant)

414

415 if normalise_sd_colours:

416 XYZ /= XYZ[..., 1]

417

418 plot_settings["color"] = np.clip(XYZ_to_plotting_colourspace(XYZ), 0, 1)

419

420 axes.plot(wavelengths, values, **plot_settings)

421

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)

436

437 return render(**settings)

438

439

440@override_style()

441 def 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.

449

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.

455

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.

463

464 Returns

465 -------

466 :class:`tuple`

467 Current figure and axes.

468

469 Examples

470 --------

471 >>> plot_single_cmfs("CIE 1931 2 Degree Standard Observer")

472 ... # doctest: +ELLIPSIS

473 (<Figure size ... with 1 Axes>, <...Axes...>)

474

475 .. image:: ../_static/Plotting_Plot_Single_CMFS.png

476 :align: center

477 :alt: plot_single_cmfs

478 """

479

480 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

481

482 settings: Dict[str, Any] = {

483 "title": f"{cmfs.display_name} - Colour Matching Functions"

484 }

485 settings.update(kwargs)

486

487 return plot_multi_cmfs((cmfs,), **settings)

488

489

490@override_style()

491 def 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.

499

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.

506

507 Other Parameters

508 ----------------

509 kwargs

510 {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},

511 See the documentation of the previously listed definitions.

512

513 Returns

514 -------

515 :class:`tuple`

516 Current figure and axes.

517

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...>)

526

527 .. image:: ../_static/Plotting_Plot_Multi_CMFS.png

528 :align: center

529 :alt: plot_multi_cmfs

530 """

531

532 cmfs = cast("List[MultiSpectralDistributions]", list(filter_cmfs(cmfs).values())) # pyright: ignore

533

534 _figure, axes = artist(**kwargs)

535

536 axes.axhline(

537 color=CONSTANTS_COLOUR_STYLE.colour.dark,

538 linestyle="--",

539 zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line,

540 )

541

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]

549

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))

555

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 )

563

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"

572

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)

582

583 return render(**settings)

584

585

586@override_style()

587 def 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.

596

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.

607

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.

615

616 Returns

617 -------

618 :class:`tuple`

619 Current figure and axes.

620

621 References

622 ----------

623 :cite:`Spiker2015a`

624

625 Examples

626 --------

627 >>> plot_single_illuminant_sd("A") # doctest: +ELLIPSIS

628 (<Figure size ... with 1 Axes>, <...Axes...>)

629

630 .. image:: ../_static/Plotting_Plot_Single_Illuminant_SD.png

631 :align: center

632 :alt: plot_single_illuminant_sd

633 """

634

635 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

636

637 title = f"Illuminant {illuminant} - {cmfs.display_name}"

638

639 illuminant = cast(

640 "SpectralDistribution",

641 first_item(filter_illuminants(illuminant).values()),

642 )

643

644 settings: Dict[str, Any] = {"title": title, "y_label": "Relative Power"}

645 settings.update(kwargs)

646

647 return plot_single_sd(illuminant, **settings)

648

649

650@override_style()

651 def 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.

657

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.

664

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.

672

673 Returns

674 -------

675 :class:`tuple`

676 Current figure and axes.

677

678 Examples

679 --------

680 >>> plot_multi_illuminant_sds(["A", "B", "C"]) # doctest: +ELLIPSIS

681 (<Figure size ... with 1 Axes>, <...Axes...>)

682

683 .. image:: ../_static/Plotting_Plot_Multi_Illuminant_SDS.png

684 :align: center

685 :alt: plot_multi_illuminant_sds

686 """

687

688 if "plot_kwargs" not in kwargs:

689 kwargs["plot_kwargs"] = {}

690

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

697

698 illuminants = cast(

699 "List[SpectralDistribution]",

700 list(filter_illuminants(illuminants).values()),

701 ) # pyright: ignore

702

703 illuminant_display_names = ", ".join(

704 [illuminant.display_name for illuminant in illuminants]

705 )

706 title = f"{illuminant_display_names} - Illuminants Spectral Distributions"

707

708 settings: Dict[str, Any] = {"title": title, "y_label": "Relative Power"}

709 settings.update(kwargs)

710

711 return plot_multi_sds(illuminants, **settings)

712

713

714@override_style(

715 **{

716 "ytick.left": False,

717 "ytick.labelleft": False,

718 }

719)

720 def 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.

730

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.

741

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.

749

750 Returns

751 -------

752 :class:`tuple`

753 Current figure and axes.

754

755 References

756 ----------

757 :cite:`Spiker2015a`

758

759 Examples

760 --------

761 >>> plot_visible_spectrum() # doctest: +ELLIPSIS

762 (<Figure size ... with 1 Axes>, <...Axes...>)

763

764 .. image:: ../_static/Plotting_Plot_Visible_Spectrum.png

765 :align: center

766 :alt: plot_visible_spectrum

767 """

768

769 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

770

771 bounding_box = (min(cmfs.wavelengths), max(cmfs.wavelengths), 0, 1)

772

773 settings: Dict[str, Any] = {"bounding_box": bounding_box, "y_label": None}

774 settings.update(kwargs)

775 settings["show"] = False

776

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 )

783

784 # Removing wavelength line as it doubles with the axes spine.

785 axes.lines[0].remove()

786

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)

794

795 return render(**settings)

796

797

798@override_style()

799 def plot_single_lightness_function(

800 function: Callable | str, **kwargs: Any

801) -> Tuple[Figure, Axes]:

802 """

803 Plot the specified *Lightness* function.

804

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.

811

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.

819

820 Returns

821 -------

822 :class:`tuple`

823 Current figure and axes.

824

825 Examples

826 --------

827 >>> plot_single_lightness_function("CIE 1976") # doctest: +ELLIPSIS

828 (<Figure size ... with 1 Axes>, <...Axes...>)

829

830 .. image:: ../_static/Plotting_Plot_Single_Lightness_Function.png

831 :align: center

832 :alt: plot_single_lightness_function

833 """

834

835 settings: Dict[str, Any] = {"title": f"{function} - Lightness Function"}

836 settings.update(kwargs)

837

838 return plot_multi_lightness_functions((function,), **settings)

839

840

841@override_style()

842 def 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.

848

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.

855

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.

863

864 Returns

865 -------

866 :class:`tuple`

867 Current figure and axes.

868

869 Examples

870 --------

871 >>> plot_multi_lightness_functions(["CIE 1976", "Wyszecki 1963"])

872 ... # doctest: +ELLIPSIS

873 (<Figure size ... with 1 Axes>, <...Axes...>)

874

875 .. image:: ../_static/Plotting_Plot_Multi_Lightness_Functions.png

876 :align: center

877 :alt: plot_multi_lightness_functions

878 """

879

880 functions_filtered = filter_passthrough(LIGHTNESS_METHODS, functions)

881

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)

890

891 with domain_range_scale("1"):

892 return plot_multi_functions(functions_filtered, **settings)

893

894

895@override_style()

896 def plot_single_luminance_function(

897 function: Callable | str, **kwargs: Any

898) -> Tuple[Figure, Axes]:

899 """

900 Plot the specified *Luminance* function.

901

902 Parameters

903 ----------

904 function

905 *Luminance* function to plot.

906

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.

914

915 Returns

916 -------

917 :class:`tuple`

918 Current figure and axes.

919

920 Examples

921 --------

922 >>> plot_single_luminance_function("CIE 1976") # doctest: +ELLIPSIS

923 (<Figure size ... with 1 Axes>, <...Axes...>)

924

925 .. image:: ../_static/Plotting_Plot_Single_Luminance_Function.png

926 :align: center

927 :alt: plot_single_luminance_function

928 """

929

930 settings: Dict[str, Any] = {"title": f"{function} - Luminance Function"}

931 settings.update(kwargs)

932

933 return plot_multi_luminance_functions((function,), **settings)

934

935

936@override_style()

937 def 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.

943

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.

950

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.

958

959 Returns

960 -------

961 :class:`tuple`

962 Current figure and axes.

963

964 Examples

965 --------

966 >>> plot_multi_luminance_functions(["CIE 1976", "Newhall 1943"])

967 ... # doctest: +ELLIPSIS

968 (<Figure size ... with 1 Axes>, <...Axes...>)

969

970 .. image:: ../_static/Plotting_Plot_Multi_Luminance_Functions.png

971 :align: center

972 :alt: plot_multi_luminance_functions

973 """

974

975 functions_filtered = filter_passthrough(LUMINANCE_METHODS, functions)

976

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)

985

986 with domain_range_scale("1"):

987 return plot_multi_functions(functions_filtered, **settings)

988

989

990@override_style()

991 def 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.

1001

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.

1012

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.

1020

1021 Returns

1022 -------

1023 :class:`tuple`

1024 Current figure and axes.

1025

1026 Examples

1027 --------

1028 >>> plot_blackbody_spectral_radiance(3500, blackbody="VY Canis Major")

1029 ... # doctest: +ELLIPSIS

1030 (<Figure size ... with 2 Axes>, <...Axes...>)

1031

1032 .. image:: ../_static/Plotting_Plot_Blackbody_Spectral_Radiance.png

1033 :align: center

1034 :alt: plot_blackbody_spectral_radiance

1035 """

1036

1037 figure = plt.figure()

1038

1039 figure.subplots_adjust(hspace=CONSTANTS_COLOUR_STYLE.geometry.short / 2)

1040

1041 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

1042

1043 sd = sd_blackbody(temperature, cmfs.shape)

1044

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

1053

1054 plot_single_sd(sd, cmfs.name, **settings)

1055

1056 axes = figure.add_subplot(212)

1057

1058 with domain_range_scale("1"):

1059 XYZ = sd_to_XYZ(sd, cmfs)

1060

1061 RGB = normalise_maximum(XYZ_to_plotting_colourspace(XYZ))

1062

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

1074

1075 figure, axes = plot_single_colour_swatch(RGB, **settings)

1076

1077 settings = {"axes": axes, "show": True}

1078 settings.update(kwargs)

1079

1080 return render(**settings)

1081

1082

1083@override_style(

1084 **{

1085 "ytick.left": False,

1086 "ytick.labelleft": False,

1087 }

1088)

1089 def 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.

1098

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.

1108

1109 Other Parameters

1110 ----------------

1111 kwargs

1112 {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},

1113 See the documentation of the previously listed definitions.

1114

1115 Returns

1116 -------

1117 :class:`tuple`

1118 Current figure and axes.

1119

1120 Examples

1121 --------

1122 >>> plot_blackbody_colours(SpectralShape(150, 12500, 50))

1123 ... # doctest: +ELLIPSIS

1124 (<Figure size ... with 1 Axes>, <...Axes...>)

1125

1126 .. image:: ../_static/Plotting_Plot_Blackbody_Colours.png

1127 :align: center

1128 :alt: plot_blackbody_colours

1129 """

1130

1131 shape = optional(shape, SpectralShape(150, 12500, 50))

1132

1133 _figure, axes = artist(**kwargs)

1134

1135 cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

1136

1137 RGB = []

1138 temperatures = []

1139

1140 for temperature in shape:

1141 sd = sd_blackbody(temperature, cmfs.shape)

1142

1143 with domain_range_scale("1"):

1144 XYZ = sd_to_XYZ(sd, cmfs)

1145

1146 RGB.append(normalise_maximum(XYZ_to_plotting_colourspace(XYZ)))

1147 temperatures.append(temperature)

1148

1149 x_min, x_max = min(temperatures), max(temperatures)

1150 y_min, y_max = 0, 1

1151

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 )

1161

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)

1170

1171 return render(**settings)