Coverage for plotting/colorimetry.py: 79%

206 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Colorimetry Plotting 

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

4 

5Define 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 

21References 

22---------- 

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

24 T. and Shaw, N. 

25""" 

26 

27from __future__ import annotations 

28 

29import typing 

30from functools import reduce 

31 

32import matplotlib.pyplot as plt 

33import numpy as np 

34 

35if typing.TYPE_CHECKING: 

36 from collections.abc import ValuesView 

37 

38from matplotlib.patches import Polygon 

39 

40if typing.TYPE_CHECKING: 

41 from matplotlib.figure import Figure 

42 from matplotlib.axes import Axes 

43 

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) 

59 

60if typing.TYPE_CHECKING: 

61 from colour.hints import ( 

62 Any, 

63 Callable, 

64 Dict, 

65 Sequence, 

66 Tuple, 

67 ) 

68 

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) 

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

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. 

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

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. 

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

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. 

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

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. 

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

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. 

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

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. 

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) 

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. 

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

799def 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() 

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. 

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

896def 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() 

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. 

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

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. 

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) 

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. 

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)