Coverage for /usr/lib/python3/dist-packages/matplotlib/colors.py: 34%

1021 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1""" 

2A module for converting numbers or color arguments to *RGB* or *RGBA*. 

3 

4*RGB* and *RGBA* are sequences of, respectively, 3 or 4 floats in the 

5range 0-1. 

6 

7This module includes functions and classes for color specification conversions, 

8and for mapping numbers to colors in a 1-D array of colors called a colormap. 

9 

10Mapping data onto colors using a colormap typically involves two steps: a data 

11array is first mapped onto the range 0-1 using a subclass of `Normalize`, 

12then this number is mapped to a color using a subclass of `Colormap`. Two 

13subclasses of `Colormap` provided here: `LinearSegmentedColormap`, which uses 

14piecewise-linear interpolation to define colormaps, and `ListedColormap`, which 

15makes a colormap from a list of colors. 

16 

17.. seealso:: 

18 

19 :doc:`/tutorials/colors/colormap-manipulation` for examples of how to 

20 make colormaps and 

21 

22 :doc:`/tutorials/colors/colormaps` for a list of built-in colormaps. 

23 

24 :doc:`/tutorials/colors/colormapnorms` for more details about data 

25 normalization 

26 

27 More colormaps are available at palettable_. 

28 

29The module also provides functions for checking whether an object can be 

30interpreted as a color (`is_color_like`), for converting such an object 

31to an RGBA tuple (`to_rgba`) or to an HTML-like hex string in the 

32"#rrggbb" format (`to_hex`), and a sequence of colors to an (n, 4) 

33RGBA array (`to_rgba_array`). Caching is used for efficiency. 

34 

35Colors that Matplotlib recognizes are listed at 

36:doc:`/tutorials/colors/colors`. 

37 

38.. _palettable: https://jiffyclub.github.io/palettable/ 

39.. _xkcd color survey: https://xkcd.com/color/rgb/ 

40""" 

41 

42import base64 

43from collections.abc import Sized, Sequence, Mapping 

44import functools 

45import importlib 

46import inspect 

47import io 

48import itertools 

49from numbers import Number 

50import re 

51from PIL import Image 

52from PIL.PngImagePlugin import PngInfo 

53 

54import matplotlib as mpl 

55import numpy as np 

56from matplotlib import _api, _cm, cbook, scale 

57from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS 

58 

59 

60class _ColorMapping(dict): 

61 def __init__(self, mapping): 

62 super().__init__(mapping) 

63 self.cache = {} 

64 

65 def __setitem__(self, key, value): 

66 super().__setitem__(key, value) 

67 self.cache.clear() 

68 

69 def __delitem__(self, key): 

70 super().__delitem__(key) 

71 self.cache.clear() 

72 

73 

74_colors_full_map = {} 

75# Set by reverse priority order. 

76_colors_full_map.update(XKCD_COLORS) 

77_colors_full_map.update({k.replace('grey', 'gray'): v 

78 for k, v in XKCD_COLORS.items() 

79 if 'grey' in k}) 

80_colors_full_map.update(CSS4_COLORS) 

81_colors_full_map.update(TABLEAU_COLORS) 

82_colors_full_map.update({k.replace('gray', 'grey'): v 

83 for k, v in TABLEAU_COLORS.items() 

84 if 'gray' in k}) 

85_colors_full_map.update(BASE_COLORS) 

86_colors_full_map = _ColorMapping(_colors_full_map) 

87 

88_REPR_PNG_SIZE = (512, 64) 

89 

90 

91def get_named_colors_mapping(): 

92 """Return the global mapping of names to named colors.""" 

93 return _colors_full_map 

94 

95 

96class ColorSequenceRegistry(Mapping): 

97 r""" 

98 Container for sequences of colors that are known to Matplotlib by name. 

99 

100 The universal registry instance is `matplotlib.color_sequences`. There 

101 should be no need for users to instantiate `.ColorSequenceRegistry` 

102 themselves. 

103 

104 Read access uses a dict-like interface mapping names to lists of colors:: 

105 

106 import matplotlib as mpl 

107 cmap = mpl.color_sequences['tab10'] 

108 

109 The returned lists are copies, so that their modification does not change 

110 the global definition of the color sequence. 

111 

112 Additional color sequences can be added via 

113 `.ColorSequenceRegistry.register`:: 

114 

115 mpl.color_sequences.register('rgb', ['r', 'g', 'b']) 

116 """ 

117 

118 _BUILTIN_COLOR_SEQUENCES = { 

119 'tab10': _cm._tab10_data, 

120 'tab20': _cm._tab20_data, 

121 'tab20b': _cm._tab20b_data, 

122 'tab20c': _cm._tab20c_data, 

123 'Pastel1': _cm._Pastel1_data, 

124 'Pastel2': _cm._Pastel2_data, 

125 'Paired': _cm._Paired_data, 

126 'Accent': _cm._Accent_data, 

127 'Dark2': _cm._Dark2_data, 

128 'Set1': _cm._Set1_data, 

129 'Set2': _cm._Set1_data, 

130 'Set3': _cm._Set1_data, 

131 } 

132 

133 def __init__(self): 

134 self._color_sequences = {**self._BUILTIN_COLOR_SEQUENCES} 

135 

136 def __getitem__(self, item): 

137 try: 

138 return list(self._color_sequences[item]) 

139 except KeyError: 

140 raise KeyError(f"{item!r} is not a known color sequence name") 

141 

142 def __iter__(self): 

143 return iter(self._color_sequences) 

144 

145 def __len__(self): 

146 return len(self._color_sequences) 

147 

148 def __str__(self): 

149 return ('ColorSequenceRegistry; available colormaps:\n' + 

150 ', '.join(f"'{name}'" for name in self)) 

151 

152 def register(self, name, color_list): 

153 """ 

154 Register a new color sequence. 

155 

156 The color sequence registry stores a copy of the given *color_list*, so 

157 that future changes to the original list do not affect the registered 

158 color sequence. Think of this as the registry taking a snapshot 

159 of *color_list* at registration. 

160 

161 Parameters 

162 ---------- 

163 name : str 

164 The name for the color sequence. 

165 

166 color_list : list of colors 

167 An iterable returning valid Matplotlib colors when iterating over. 

168 Note however that the returned color sequence will always be a 

169 list regardless of the input type. 

170 

171 """ 

172 if name in self._BUILTIN_COLOR_SEQUENCES: 

173 raise ValueError(f"{name!r} is a reserved name for a builtin " 

174 "color sequence") 

175 

176 color_list = list(color_list) # force copy and coerce type to list 

177 for color in color_list: 

178 try: 

179 to_rgba(color) 

180 except ValueError: 

181 raise ValueError( 

182 f"{color!r} is not a valid color specification") 

183 

184 self._color_sequences[name] = color_list 

185 

186 def unregister(self, name): 

187 """ 

188 Remove a sequence from the registry. 

189 

190 You cannot remove built-in color sequences. 

191 

192 If the name is not registered, returns with no error. 

193 """ 

194 if name in self._BUILTIN_COLOR_SEQUENCES: 

195 raise ValueError( 

196 f"Cannot unregister builtin color sequence {name!r}") 

197 self._color_sequences.pop(name, None) 

198 

199 

200_color_sequences = ColorSequenceRegistry() 

201 

202 

203def _sanitize_extrema(ex): 

204 if ex is None: 

205 return ex 

206 try: 

207 ret = ex.item() 

208 except AttributeError: 

209 ret = float(ex) 

210 return ret 

211 

212 

213def _is_nth_color(c): 

214 """Return whether *c* can be interpreted as an item in the color cycle.""" 

215 return isinstance(c, str) and re.match(r"\AC[0-9]+\Z", c) 

216 

217 

218def is_color_like(c): 

219 """Return whether *c* can be interpreted as an RGB(A) color.""" 

220 # Special-case nth color syntax because it cannot be parsed during setup. 

221 if _is_nth_color(c): 

222 return True 

223 try: 

224 to_rgba(c) 

225 except ValueError: 

226 return False 

227 else: 

228 return True 

229 

230 

231def _has_alpha_channel(c): 

232 """Return whether *c* is a color with an alpha channel.""" 

233 # 4-element sequences are interpreted as r, g, b, a 

234 return not isinstance(c, str) and len(c) == 4 

235 

236 

237def _check_color_like(**kwargs): 

238 """ 

239 For each *key, value* pair in *kwargs*, check that *value* is color-like. 

240 """ 

241 for k, v in kwargs.items(): 

242 if not is_color_like(v): 

243 raise ValueError(f"{v!r} is not a valid value for {k}") 

244 

245 

246def same_color(c1, c2): 

247 """ 

248 Return whether the colors *c1* and *c2* are the same. 

249 

250 *c1*, *c2* can be single colors or lists/arrays of colors. 

251 """ 

252 c1 = to_rgba_array(c1) 

253 c2 = to_rgba_array(c2) 

254 n1 = max(c1.shape[0], 1) # 'none' results in shape (0, 4), but is 1-elem 

255 n2 = max(c2.shape[0], 1) # 'none' results in shape (0, 4), but is 1-elem 

256 

257 if n1 != n2: 

258 raise ValueError('Different number of elements passed.') 

259 # The following shape test is needed to correctly handle comparisons with 

260 # 'none', which results in a shape (0, 4) array and thus cannot be tested 

261 # via value comparison. 

262 return c1.shape == c2.shape and (c1 == c2).all() 

263 

264 

265def to_rgba(c, alpha=None): 

266 """ 

267 Convert *c* to an RGBA color. 

268 

269 Parameters 

270 ---------- 

271 c : Matplotlib color or ``np.ma.masked`` 

272 

273 alpha : float, optional 

274 If *alpha* is given, force the alpha value of the returned RGBA tuple 

275 to *alpha*. 

276 

277 If None, the alpha value from *c* is used. If *c* does not have an 

278 alpha channel, then alpha defaults to 1. 

279 

280 *alpha* is ignored for the color value ``"none"`` (case-insensitive), 

281 which always maps to ``(0, 0, 0, 0)``. 

282 

283 Returns 

284 ------- 

285 tuple 

286 Tuple of floats ``(r, g, b, a)``, where each channel (red, green, blue, 

287 alpha) can assume values between 0 and 1. 

288 """ 

289 # Special-case nth color syntax because it should not be cached. 

290 if _is_nth_color(c): 

291 prop_cycler = mpl.rcParams['axes.prop_cycle'] 

292 colors = prop_cycler.by_key().get('color', ['k']) 

293 c = colors[int(c[1:]) % len(colors)] 

294 try: 

295 rgba = _colors_full_map.cache[c, alpha] 

296 except (KeyError, TypeError): # Not in cache, or unhashable. 

297 rgba = None 

298 if rgba is None: # Suppress exception chaining of cache lookup failure. 

299 rgba = _to_rgba_no_colorcycle(c, alpha) 

300 try: 

301 _colors_full_map.cache[c, alpha] = rgba 

302 except TypeError: 

303 pass 

304 return rgba 

305 

306 

307def _to_rgba_no_colorcycle(c, alpha=None): 

308 """ 

309 Convert *c* to an RGBA color, with no support for color-cycle syntax. 

310 

311 If *alpha* is given, force the alpha value of the returned RGBA tuple 

312 to *alpha*. Otherwise, the alpha value from *c* is used, if it has alpha 

313 information, or defaults to 1. 

314 

315 *alpha* is ignored for the color value ``"none"`` (case-insensitive), 

316 which always maps to ``(0, 0, 0, 0)``. 

317 """ 

318 orig_c = c 

319 if c is np.ma.masked: 

320 return (0., 0., 0., 0.) 

321 if isinstance(c, str): 

322 if c.lower() == "none": 

323 return (0., 0., 0., 0.) 

324 # Named color. 

325 try: 

326 # This may turn c into a non-string, so we check again below. 

327 c = _colors_full_map[c] 

328 except KeyError: 

329 if len(orig_c) != 1: 

330 try: 

331 c = _colors_full_map[c.lower()] 

332 except KeyError: 

333 pass 

334 if isinstance(c, str): 

335 # hex color in #rrggbb format. 

336 match = re.match(r"\A#[a-fA-F0-9]{6}\Z", c) 

337 if match: 

338 return (tuple(int(n, 16) / 255 

339 for n in [c[1:3], c[3:5], c[5:7]]) 

340 + (alpha if alpha is not None else 1.,)) 

341 # hex color in #rgb format, shorthand for #rrggbb. 

342 match = re.match(r"\A#[a-fA-F0-9]{3}\Z", c) 

343 if match: 

344 return (tuple(int(n, 16) / 255 

345 for n in [c[1]*2, c[2]*2, c[3]*2]) 

346 + (alpha if alpha is not None else 1.,)) 

347 # hex color with alpha in #rrggbbaa format. 

348 match = re.match(r"\A#[a-fA-F0-9]{8}\Z", c) 

349 if match: 

350 color = [int(n, 16) / 255 

351 for n in [c[1:3], c[3:5], c[5:7], c[7:9]]] 

352 if alpha is not None: 

353 color[-1] = alpha 

354 return tuple(color) 

355 # hex color with alpha in #rgba format, shorthand for #rrggbbaa. 

356 match = re.match(r"\A#[a-fA-F0-9]{4}\Z", c) 

357 if match: 

358 color = [int(n, 16) / 255 

359 for n in [c[1]*2, c[2]*2, c[3]*2, c[4]*2]] 

360 if alpha is not None: 

361 color[-1] = alpha 

362 return tuple(color) 

363 # string gray. 

364 try: 

365 c = float(c) 

366 except ValueError: 

367 pass 

368 else: 

369 if not (0 <= c <= 1): 

370 raise ValueError( 

371 f"Invalid string grayscale value {orig_c!r}. " 

372 f"Value must be within 0-1 range") 

373 return c, c, c, alpha if alpha is not None else 1. 

374 raise ValueError(f"Invalid RGBA argument: {orig_c!r}") 

375 # turn 2-D array into 1-D array 

376 if isinstance(c, np.ndarray): 

377 if c.ndim == 2 and c.shape[0] == 1: 

378 c = c.reshape(-1) 

379 # tuple color. 

380 if not np.iterable(c): 

381 raise ValueError(f"Invalid RGBA argument: {orig_c!r}") 

382 if len(c) not in [3, 4]: 

383 raise ValueError("RGBA sequence should have length 3 or 4") 

384 if not all(isinstance(x, Number) for x in c): 

385 # Checks that don't work: `map(float, ...)`, `np.array(..., float)` and 

386 # `np.array(...).astype(float)` would all convert "0.5" to 0.5. 

387 raise ValueError(f"Invalid RGBA argument: {orig_c!r}") 

388 # Return a tuple to prevent the cached value from being modified. 

389 c = tuple(map(float, c)) 

390 if len(c) == 3 and alpha is None: 

391 alpha = 1 

392 if alpha is not None: 

393 c = c[:3] + (alpha,) 

394 if any(elem < 0 or elem > 1 for elem in c): 

395 raise ValueError("RGBA values should be within 0-1 range") 

396 return c 

397 

398 

399def to_rgba_array(c, alpha=None): 

400 """ 

401 Convert *c* to a (n, 4) array of RGBA colors. 

402 

403 Parameters 

404 ---------- 

405 c : Matplotlib color or array of colors 

406 If *c* is a masked array, an ndarray is returned with a (0, 0, 0, 0) 

407 row for each masked value or row in *c*. 

408 

409 alpha : float or sequence of floats, optional 

410 If *alpha* is given, force the alpha value of the returned RGBA tuple 

411 to *alpha*. 

412 

413 If None, the alpha value from *c* is used. If *c* does not have an 

414 alpha channel, then alpha defaults to 1. 

415 

416 *alpha* is ignored for the color value ``"none"`` (case-insensitive), 

417 which always maps to ``(0, 0, 0, 0)``. 

418 

419 If *alpha* is a sequence and *c* is a single color, *c* will be 

420 repeated to match the length of *alpha*. 

421 

422 Returns 

423 ------- 

424 array 

425 (n, 4) array of RGBA colors, where each channel (red, green, blue, 

426 alpha) can assume values between 0 and 1. 

427 """ 

428 # Special-case inputs that are already arrays, for performance. (If the 

429 # array has the wrong kind or shape, raise the error during one-at-a-time 

430 # conversion.) 

431 if np.iterable(alpha): 

432 alpha = np.asarray(alpha).ravel() 

433 if (isinstance(c, np.ndarray) and c.dtype.kind in "if" 

434 and c.ndim == 2 and c.shape[1] in [3, 4]): 

435 mask = c.mask.any(axis=1) if np.ma.is_masked(c) else None 

436 c = np.ma.getdata(c) 

437 if np.iterable(alpha): 

438 if c.shape[0] == 1 and alpha.shape[0] > 1: 

439 c = np.tile(c, (alpha.shape[0], 1)) 

440 elif c.shape[0] != alpha.shape[0]: 

441 raise ValueError("The number of colors must match the number" 

442 " of alpha values if there are more than one" 

443 " of each.") 

444 if c.shape[1] == 3: 

445 result = np.column_stack([c, np.zeros(len(c))]) 

446 result[:, -1] = alpha if alpha is not None else 1. 

447 elif c.shape[1] == 4: 

448 result = c.copy() 

449 if alpha is not None: 

450 result[:, -1] = alpha 

451 if mask is not None: 

452 result[mask] = 0 

453 if np.any((result < 0) | (result > 1)): 

454 raise ValueError("RGBA values should be within 0-1 range") 

455 return result 

456 # Handle single values. 

457 # Note that this occurs *after* handling inputs that are already arrays, as 

458 # `to_rgba(c, alpha)` (below) is expensive for such inputs, due to the need 

459 # to format the array in the ValueError message(!). 

460 if cbook._str_lower_equal(c, "none"): 

461 return np.zeros((0, 4), float) 

462 try: 

463 if np.iterable(alpha): 

464 return np.array([to_rgba(c, a) for a in alpha], float) 

465 else: 

466 return np.array([to_rgba(c, alpha)], float) 

467 except (ValueError, TypeError): 

468 pass 

469 

470 if isinstance(c, str): 

471 raise ValueError(f"{c!r} is not a valid color value.") 

472 

473 if len(c) == 0: 

474 return np.zeros((0, 4), float) 

475 

476 # Quick path if the whole sequence can be directly converted to a numpy 

477 # array in one shot. 

478 if isinstance(c, Sequence): 

479 lens = {len(cc) if isinstance(cc, (list, tuple)) else -1 for cc in c} 

480 if lens == {3}: 

481 rgba = np.column_stack([c, np.ones(len(c))]) 

482 elif lens == {4}: 

483 rgba = np.array(c) 

484 else: 

485 rgba = np.array([to_rgba(cc) for cc in c]) 

486 else: 

487 rgba = np.array([to_rgba(cc) for cc in c]) 

488 

489 if alpha is not None: 

490 rgba[:, 3] = alpha 

491 return rgba 

492 

493 

494def to_rgb(c): 

495 """Convert *c* to an RGB color, silently dropping the alpha channel.""" 

496 return to_rgba(c)[:3] 

497 

498 

499def to_hex(c, keep_alpha=False): 

500 """ 

501 Convert *c* to a hex color. 

502 

503 Parameters 

504 ---------- 

505 c : :doc:`color </tutorials/colors/colors>` or `numpy.ma.masked` 

506 

507 keep_alpha : bool, default: False 

508 If False, use the ``#rrggbb`` format, otherwise use ``#rrggbbaa``. 

509 

510 Returns 

511 ------- 

512 str 

513 ``#rrggbb`` or ``#rrggbbaa`` hex color string 

514 """ 

515 c = to_rgba(c) 

516 if not keep_alpha: 

517 c = c[:3] 

518 return "#" + "".join(format(int(round(val * 255)), "02x") for val in c) 

519 

520 

521### Backwards-compatible color-conversion API 

522 

523 

524cnames = CSS4_COLORS 

525hexColorPattern = re.compile(r"\A#[a-fA-F0-9]{6}\Z") 

526rgb2hex = to_hex 

527hex2color = to_rgb 

528 

529 

530class ColorConverter: 

531 """ 

532 A class only kept for backwards compatibility. 

533 

534 Its functionality is entirely provided by module-level functions. 

535 """ 

536 colors = _colors_full_map 

537 cache = _colors_full_map.cache 

538 to_rgb = staticmethod(to_rgb) 

539 to_rgba = staticmethod(to_rgba) 

540 to_rgba_array = staticmethod(to_rgba_array) 

541 

542 

543colorConverter = ColorConverter() 

544 

545 

546### End of backwards-compatible color-conversion API 

547 

548 

549def _create_lookup_table(N, data, gamma=1.0): 

550 r""" 

551 Create an *N* -element 1D lookup table. 

552 

553 This assumes a mapping :math:`f : [0, 1] \rightarrow [0, 1]`. The returned 

554 data is an array of N values :math:`y = f(x)` where x is sampled from 

555 [0, 1]. 

556 

557 By default (*gamma* = 1) x is equidistantly sampled from [0, 1]. The 

558 *gamma* correction factor :math:`\gamma` distorts this equidistant 

559 sampling by :math:`x \rightarrow x^\gamma`. 

560 

561 Parameters 

562 ---------- 

563 N : int 

564 The number of elements of the created lookup table; at least 1. 

565 

566 data : (M, 3) array-like or callable 

567 Defines the mapping :math:`f`. 

568 

569 If a (M, 3) array-like, the rows define values (x, y0, y1). The x 

570 values must start with x=0, end with x=1, and all x values be in 

571 increasing order. 

572 

573 A value between :math:`x_i` and :math:`x_{i+1}` is mapped to the range 

574 :math:`y^1_{i-1} \ldots y^0_i` by linear interpolation. 

575 

576 For the simple case of a y-continuous mapping, y0 and y1 are identical. 

577 

578 The two values of y are to allow for discontinuous mapping functions. 

579 E.g. a sawtooth with a period of 0.2 and an amplitude of 1 would be:: 

580 

581 [(0, 1, 0), (0.2, 1, 0), (0.4, 1, 0), ..., [(1, 1, 0)] 

582 

583 In the special case of ``N == 1``, by convention the returned value 

584 is y0 for x == 1. 

585 

586 If *data* is a callable, it must accept and return numpy arrays:: 

587 

588 data(x : ndarray) -> ndarray 

589 

590 and map values between 0 - 1 to 0 - 1. 

591 

592 gamma : float 

593 Gamma correction factor for input distribution x of the mapping. 

594 

595 See also https://en.wikipedia.org/wiki/Gamma_correction. 

596 

597 Returns 

598 ------- 

599 array 

600 The lookup table where ``lut[x * (N-1)]`` gives the closest value 

601 for values of x between 0 and 1. 

602 

603 Notes 

604 ----- 

605 This function is internally used for `.LinearSegmentedColormap`. 

606 """ 

607 

608 if callable(data): 

609 xind = np.linspace(0, 1, N) ** gamma 

610 lut = np.clip(np.array(data(xind), dtype=float), 0, 1) 

611 return lut 

612 

613 try: 

614 adata = np.array(data) 

615 except Exception as err: 

616 raise TypeError("data must be convertible to an array") from err 

617 _api.check_shape((None, 3), data=adata) 

618 

619 x = adata[:, 0] 

620 y0 = adata[:, 1] 

621 y1 = adata[:, 2] 

622 

623 if x[0] != 0. or x[-1] != 1.0: 

624 raise ValueError( 

625 "data mapping points must start with x=0 and end with x=1") 

626 if (np.diff(x) < 0).any(): 

627 raise ValueError("data mapping points must have x in increasing order") 

628 # begin generation of lookup table 

629 if N == 1: 

630 # convention: use the y = f(x=1) value for a 1-element lookup table 

631 lut = np.array(y0[-1]) 

632 else: 

633 x = x * (N - 1) 

634 xind = (N - 1) * np.linspace(0, 1, N) ** gamma 

635 ind = np.searchsorted(x, xind)[1:-1] 

636 

637 distance = (xind[1:-1] - x[ind - 1]) / (x[ind] - x[ind - 1]) 

638 lut = np.concatenate([ 

639 [y1[0]], 

640 distance * (y0[ind] - y1[ind - 1]) + y1[ind - 1], 

641 [y0[-1]], 

642 ]) 

643 # ensure that the lut is confined to values between 0 and 1 by clipping it 

644 return np.clip(lut, 0.0, 1.0) 

645 

646 

647class Colormap: 

648 """ 

649 Baseclass for all scalar to RGBA mappings. 

650 

651 Typically, Colormap instances are used to convert data values (floats) 

652 from the interval ``[0, 1]`` to the RGBA color that the respective 

653 Colormap represents. For scaling of data into the ``[0, 1]`` interval see 

654 `matplotlib.colors.Normalize`. Subclasses of `matplotlib.cm.ScalarMappable` 

655 make heavy use of this ``data -> normalize -> map-to-color`` processing 

656 chain. 

657 """ 

658 

659 def __init__(self, name, N=256): 

660 """ 

661 Parameters 

662 ---------- 

663 name : str 

664 The name of the colormap. 

665 N : int 

666 The number of RGB quantization levels. 

667 """ 

668 self.name = name 

669 self.N = int(N) # ensure that N is always int 

670 self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything. 

671 self._rgba_under = None 

672 self._rgba_over = None 

673 self._i_under = self.N 

674 self._i_over = self.N + 1 

675 self._i_bad = self.N + 2 

676 self._isinit = False 

677 #: When this colormap exists on a scalar mappable and colorbar_extend 

678 #: is not False, colorbar creation will pick up ``colorbar_extend`` as 

679 #: the default value for the ``extend`` keyword in the 

680 #: `matplotlib.colorbar.Colorbar` constructor. 

681 self.colorbar_extend = False 

682 

683 def __call__(self, X, alpha=None, bytes=False): 

684 """ 

685 Parameters 

686 ---------- 

687 X : float or int, ndarray or scalar 

688 The data value(s) to convert to RGBA. 

689 For floats, X should be in the interval ``[0.0, 1.0]`` to 

690 return the RGBA values ``X*100`` percent along the Colormap line. 

691 For integers, X should be in the interval ``[0, Colormap.N)`` to 

692 return RGBA values *indexed* from the Colormap with index ``X``. 

693 alpha : float or array-like or None 

694 Alpha must be a scalar between 0 and 1, a sequence of such 

695 floats with shape matching X, or None. 

696 bytes : bool 

697 If False (default), the returned RGBA values will be floats in the 

698 interval ``[0, 1]`` otherwise they will be uint8s in the interval 

699 ``[0, 255]``. 

700 

701 Returns 

702 ------- 

703 Tuple of RGBA values if X is scalar, otherwise an array of 

704 RGBA values with a shape of ``X.shape + (4, )``. 

705 """ 

706 if not self._isinit: 

707 self._init() 

708 

709 # Take the bad mask from a masked array, or in all other cases defer 

710 # np.isnan() to after we have converted to an array. 

711 mask_bad = X.mask if np.ma.is_masked(X) else None 

712 xa = np.array(X, copy=True) 

713 if mask_bad is None: 

714 mask_bad = np.isnan(xa) 

715 if not xa.dtype.isnative: 

716 xa = xa.byteswap().newbyteorder() # Native byteorder is faster. 

717 if xa.dtype.kind == "f": 

718 with np.errstate(invalid="ignore"): 

719 xa *= self.N 

720 # Negative values are out of range, but astype(int) would 

721 # truncate them towards zero. 

722 xa[xa < 0] = -1 

723 # xa == 1 (== N after multiplication) is not out of range. 

724 xa[xa == self.N] = self.N - 1 

725 # Avoid converting large positive values to negative integers. 

726 np.clip(xa, -1, self.N, out=xa) 

727 xa = xa.astype(int) 

728 # Set the over-range indices before the under-range; 

729 # otherwise the under-range values get converted to over-range. 

730 xa[xa > self.N - 1] = self._i_over 

731 xa[xa < 0] = self._i_under 

732 xa[mask_bad] = self._i_bad 

733 

734 lut = self._lut 

735 if bytes: 

736 lut = (lut * 255).astype(np.uint8) 

737 

738 rgba = lut.take(xa, axis=0, mode='clip') 

739 

740 if alpha is not None: 

741 alpha = np.clip(alpha, 0, 1) 

742 if bytes: 

743 alpha *= 255 # Will be cast to uint8 upon assignment. 

744 if alpha.shape not in [(), xa.shape]: 

745 raise ValueError( 

746 f"alpha is array-like but its shape {alpha.shape} does " 

747 f"not match that of X {xa.shape}") 

748 rgba[..., -1] = alpha 

749 

750 # If the "bad" color is all zeros, then ignore alpha input. 

751 if (lut[-1] == 0).all() and np.any(mask_bad): 

752 if np.iterable(mask_bad) and mask_bad.shape == xa.shape: 

753 rgba[mask_bad] = (0, 0, 0, 0) 

754 else: 

755 rgba[..., :] = (0, 0, 0, 0) 

756 

757 if not np.iterable(X): 

758 rgba = tuple(rgba) 

759 return rgba 

760 

761 def __copy__(self): 

762 cls = self.__class__ 

763 cmapobject = cls.__new__(cls) 

764 cmapobject.__dict__.update(self.__dict__) 

765 if self._isinit: 

766 cmapobject._lut = np.copy(self._lut) 

767 return cmapobject 

768 

769 def __eq__(self, other): 

770 if (not isinstance(other, Colormap) or self.name != other.name or 

771 self.colorbar_extend != other.colorbar_extend): 

772 return False 

773 # To compare lookup tables the Colormaps have to be initialized 

774 if not self._isinit: 

775 self._init() 

776 if not other._isinit: 

777 other._init() 

778 return np.array_equal(self._lut, other._lut) 

779 

780 def get_bad(self): 

781 """Get the color for masked values.""" 

782 if not self._isinit: 

783 self._init() 

784 return np.array(self._lut[self._i_bad]) 

785 

786 def set_bad(self, color='k', alpha=None): 

787 """Set the color for masked values.""" 

788 self._rgba_bad = to_rgba(color, alpha) 

789 if self._isinit: 

790 self._set_extremes() 

791 

792 def get_under(self): 

793 """Get the color for low out-of-range values.""" 

794 if not self._isinit: 

795 self._init() 

796 return np.array(self._lut[self._i_under]) 

797 

798 def set_under(self, color='k', alpha=None): 

799 """Set the color for low out-of-range values.""" 

800 self._rgba_under = to_rgba(color, alpha) 

801 if self._isinit: 

802 self._set_extremes() 

803 

804 def get_over(self): 

805 """Get the color for high out-of-range values.""" 

806 if not self._isinit: 

807 self._init() 

808 return np.array(self._lut[self._i_over]) 

809 

810 def set_over(self, color='k', alpha=None): 

811 """Set the color for high out-of-range values.""" 

812 self._rgba_over = to_rgba(color, alpha) 

813 if self._isinit: 

814 self._set_extremes() 

815 

816 def set_extremes(self, *, bad=None, under=None, over=None): 

817 """ 

818 Set the colors for masked (*bad*) values and, when ``norm.clip = 

819 False``, low (*under*) and high (*over*) out-of-range values. 

820 """ 

821 if bad is not None: 

822 self.set_bad(bad) 

823 if under is not None: 

824 self.set_under(under) 

825 if over is not None: 

826 self.set_over(over) 

827 

828 def with_extremes(self, *, bad=None, under=None, over=None): 

829 """ 

830 Return a copy of the colormap, for which the colors for masked (*bad*) 

831 values and, when ``norm.clip = False``, low (*under*) and high (*over*) 

832 out-of-range values, have been set accordingly. 

833 """ 

834 new_cm = self.copy() 

835 new_cm.set_extremes(bad=bad, under=under, over=over) 

836 return new_cm 

837 

838 def _set_extremes(self): 

839 if self._rgba_under: 

840 self._lut[self._i_under] = self._rgba_under 

841 else: 

842 self._lut[self._i_under] = self._lut[0] 

843 if self._rgba_over: 

844 self._lut[self._i_over] = self._rgba_over 

845 else: 

846 self._lut[self._i_over] = self._lut[self.N - 1] 

847 self._lut[self._i_bad] = self._rgba_bad 

848 

849 def _init(self): 

850 """Generate the lookup table, ``self._lut``.""" 

851 raise NotImplementedError("Abstract class only") 

852 

853 def is_gray(self): 

854 """Return whether the colormap is grayscale.""" 

855 if not self._isinit: 

856 self._init() 

857 return (np.all(self._lut[:, 0] == self._lut[:, 1]) and 

858 np.all(self._lut[:, 0] == self._lut[:, 2])) 

859 

860 def resampled(self, lutsize): 

861 """Return a new colormap with *lutsize* entries.""" 

862 if hasattr(self, '_resample'): 

863 _api.warn_external( 

864 "The ability to resample a color map is now public API " 

865 f"However the class {type(self)} still only implements " 

866 "the previous private _resample method. Please update " 

867 "your class." 

868 ) 

869 return self._resample(lutsize) 

870 

871 raise NotImplementedError() 

872 

873 def reversed(self, name=None): 

874 """ 

875 Return a reversed instance of the Colormap. 

876 

877 .. note:: This function is not implemented for the base class. 

878 

879 Parameters 

880 ---------- 

881 name : str, optional 

882 The name for the reversed colormap. If None, the 

883 name is set to ``self.name + "_r"``. 

884 

885 See Also 

886 -------- 

887 LinearSegmentedColormap.reversed 

888 ListedColormap.reversed 

889 """ 

890 raise NotImplementedError() 

891 

892 def _repr_png_(self): 

893 """Generate a PNG representation of the Colormap.""" 

894 X = np.tile(np.linspace(0, 1, _REPR_PNG_SIZE[0]), 

895 (_REPR_PNG_SIZE[1], 1)) 

896 pixels = self(X, bytes=True) 

897 png_bytes = io.BytesIO() 

898 title = self.name + ' colormap' 

899 author = f'Matplotlib v{mpl.__version__}, https://matplotlib.org' 

900 pnginfo = PngInfo() 

901 pnginfo.add_text('Title', title) 

902 pnginfo.add_text('Description', title) 

903 pnginfo.add_text('Author', author) 

904 pnginfo.add_text('Software', author) 

905 Image.fromarray(pixels).save(png_bytes, format='png', pnginfo=pnginfo) 

906 return png_bytes.getvalue() 

907 

908 def _repr_html_(self): 

909 """Generate an HTML representation of the Colormap.""" 

910 png_bytes = self._repr_png_() 

911 png_base64 = base64.b64encode(png_bytes).decode('ascii') 

912 def color_block(color): 

913 hex_color = to_hex(color, keep_alpha=True) 

914 return (f'<div title="{hex_color}" ' 

915 'style="display: inline-block; ' 

916 'width: 1em; height: 1em; ' 

917 'margin: 0; ' 

918 'vertical-align: middle; ' 

919 'border: 1px solid #555; ' 

920 f'background-color: {hex_color};"></div>') 

921 

922 return ('<div style="vertical-align: middle;">' 

923 f'<strong>{self.name}</strong> ' 

924 '</div>' 

925 '<div class="cmap"><img ' 

926 f'alt="{self.name} colormap" ' 

927 f'title="{self.name}" ' 

928 'style="border: 1px solid #555;" ' 

929 f'src="data:image/png;base64,{png_base64}"></div>' 

930 '<div style="vertical-align: middle; ' 

931 f'max-width: {_REPR_PNG_SIZE[0]+2}px; ' 

932 'display: flex; justify-content: space-between;">' 

933 '<div style="float: left;">' 

934 f'{color_block(self.get_under())} under' 

935 '</div>' 

936 '<div style="margin: 0 auto; display: inline-block;">' 

937 f'bad {color_block(self.get_bad())}' 

938 '</div>' 

939 '<div style="float: right;">' 

940 f'over {color_block(self.get_over())}' 

941 '</div>') 

942 

943 def copy(self): 

944 """Return a copy of the colormap.""" 

945 return self.__copy__() 

946 

947 

948class LinearSegmentedColormap(Colormap): 

949 """ 

950 Colormap objects based on lookup tables using linear segments. 

951 

952 The lookup table is generated using linear interpolation for each 

953 primary color, with the 0-1 domain divided into any number of 

954 segments. 

955 """ 

956 

957 def __init__(self, name, segmentdata, N=256, gamma=1.0): 

958 """ 

959 Create colormap from linear mapping segments 

960 

961 segmentdata argument is a dictionary with a red, green and blue 

962 entries. Each entry should be a list of *x*, *y0*, *y1* tuples, 

963 forming rows in a table. Entries for alpha are optional. 

964 

965 Example: suppose you want red to increase from 0 to 1 over 

966 the bottom half, green to do the same over the middle half, 

967 and blue over the top half. Then you would use:: 

968 

969 cdict = {'red': [(0.0, 0.0, 0.0), 

970 (0.5, 1.0, 1.0), 

971 (1.0, 1.0, 1.0)], 

972 

973 'green': [(0.0, 0.0, 0.0), 

974 (0.25, 0.0, 0.0), 

975 (0.75, 1.0, 1.0), 

976 (1.0, 1.0, 1.0)], 

977 

978 'blue': [(0.0, 0.0, 0.0), 

979 (0.5, 0.0, 0.0), 

980 (1.0, 1.0, 1.0)]} 

981 

982 Each row in the table for a given color is a sequence of 

983 *x*, *y0*, *y1* tuples. In each sequence, *x* must increase 

984 monotonically from 0 to 1. For any input value *z* falling 

985 between *x[i]* and *x[i+1]*, the output value of a given color 

986 will be linearly interpolated between *y1[i]* and *y0[i+1]*:: 

987 

988 row i: x y0 y1 

989 / 

990 / 

991 row i+1: x y0 y1 

992 

993 Hence y0 in the first row and y1 in the last row are never used. 

994 

995 See Also 

996 -------- 

997 LinearSegmentedColormap.from_list 

998 Static method; factory function for generating a smoothly-varying 

999 LinearSegmentedColormap. 

1000 """ 

1001 # True only if all colors in map are identical; needed for contouring. 

1002 self.monochrome = False 

1003 super().__init__(name, N) 

1004 self._segmentdata = segmentdata 

1005 self._gamma = gamma 

1006 

1007 def _init(self): 

1008 self._lut = np.ones((self.N + 3, 4), float) 

1009 self._lut[:-3, 0] = _create_lookup_table( 

1010 self.N, self._segmentdata['red'], self._gamma) 

1011 self._lut[:-3, 1] = _create_lookup_table( 

1012 self.N, self._segmentdata['green'], self._gamma) 

1013 self._lut[:-3, 2] = _create_lookup_table( 

1014 self.N, self._segmentdata['blue'], self._gamma) 

1015 if 'alpha' in self._segmentdata: 

1016 self._lut[:-3, 3] = _create_lookup_table( 

1017 self.N, self._segmentdata['alpha'], 1) 

1018 self._isinit = True 

1019 self._set_extremes() 

1020 

1021 def set_gamma(self, gamma): 

1022 """Set a new gamma value and regenerate colormap.""" 

1023 self._gamma = gamma 

1024 self._init() 

1025 

1026 @staticmethod 

1027 def from_list(name, colors, N=256, gamma=1.0): 

1028 """ 

1029 Create a `LinearSegmentedColormap` from a list of colors. 

1030 

1031 Parameters 

1032 ---------- 

1033 name : str 

1034 The name of the colormap. 

1035 colors : array-like of colors or array-like of (value, color) 

1036 If only colors are given, they are equidistantly mapped from the 

1037 range :math:`[0, 1]`; i.e. 0 maps to ``colors[0]`` and 1 maps to 

1038 ``colors[-1]``. 

1039 If (value, color) pairs are given, the mapping is from *value* 

1040 to *color*. This can be used to divide the range unevenly. 

1041 N : int 

1042 The number of RGB quantization levels. 

1043 gamma : float 

1044 """ 

1045 if not np.iterable(colors): 

1046 raise ValueError('colors must be iterable') 

1047 

1048 if (isinstance(colors[0], Sized) and len(colors[0]) == 2 

1049 and not isinstance(colors[0], str)): 

1050 # List of value, color pairs 

1051 vals, colors = zip(*colors) 

1052 else: 

1053 vals = np.linspace(0, 1, len(colors)) 

1054 

1055 r, g, b, a = to_rgba_array(colors).T 

1056 cdict = { 

1057 "red": np.column_stack([vals, r, r]), 

1058 "green": np.column_stack([vals, g, g]), 

1059 "blue": np.column_stack([vals, b, b]), 

1060 "alpha": np.column_stack([vals, a, a]), 

1061 } 

1062 

1063 return LinearSegmentedColormap(name, cdict, N, gamma) 

1064 

1065 def resampled(self, lutsize): 

1066 """Return a new colormap with *lutsize* entries.""" 

1067 new_cmap = LinearSegmentedColormap(self.name, self._segmentdata, 

1068 lutsize) 

1069 new_cmap._rgba_over = self._rgba_over 

1070 new_cmap._rgba_under = self._rgba_under 

1071 new_cmap._rgba_bad = self._rgba_bad 

1072 return new_cmap 

1073 

1074 # Helper ensuring picklability of the reversed cmap. 

1075 @staticmethod 

1076 def _reverser(func, x): 

1077 return func(1 - x) 

1078 

1079 def reversed(self, name=None): 

1080 """ 

1081 Return a reversed instance of the Colormap. 

1082 

1083 Parameters 

1084 ---------- 

1085 name : str, optional 

1086 The name for the reversed colormap. If None, the 

1087 name is set to ``self.name + "_r"``. 

1088 

1089 Returns 

1090 ------- 

1091 LinearSegmentedColormap 

1092 The reversed colormap. 

1093 """ 

1094 if name is None: 

1095 name = self.name + "_r" 

1096 

1097 # Using a partial object keeps the cmap picklable. 

1098 data_r = {key: (functools.partial(self._reverser, data) 

1099 if callable(data) else 

1100 [(1.0 - x, y1, y0) for x, y0, y1 in reversed(data)]) 

1101 for key, data in self._segmentdata.items()} 

1102 

1103 new_cmap = LinearSegmentedColormap(name, data_r, self.N, self._gamma) 

1104 # Reverse the over/under values too 

1105 new_cmap._rgba_over = self._rgba_under 

1106 new_cmap._rgba_under = self._rgba_over 

1107 new_cmap._rgba_bad = self._rgba_bad 

1108 return new_cmap 

1109 

1110 

1111class ListedColormap(Colormap): 

1112 """ 

1113 Colormap object generated from a list of colors. 

1114 

1115 This may be most useful when indexing directly into a colormap, 

1116 but it can also be used to generate special colormaps for ordinary 

1117 mapping. 

1118 

1119 Parameters 

1120 ---------- 

1121 colors : list, array 

1122 List of Matplotlib color specifications, or an equivalent Nx3 or Nx4 

1123 floating point array (*N* RGB or RGBA values). 

1124 name : str, optional 

1125 String to identify the colormap. 

1126 N : int, optional 

1127 Number of entries in the map. The default is *None*, in which case 

1128 there is one colormap entry for each element in the list of colors. 

1129 If :: 

1130 

1131 N < len(colors) 

1132 

1133 the list will be truncated at *N*. If :: 

1134 

1135 N > len(colors) 

1136 

1137 the list will be extended by repetition. 

1138 """ 

1139 def __init__(self, colors, name='from_list', N=None): 

1140 self.monochrome = False # Are all colors identical? (for contour.py) 

1141 if N is None: 

1142 self.colors = colors 

1143 N = len(colors) 

1144 else: 

1145 if isinstance(colors, str): 

1146 self.colors = [colors] * N 

1147 self.monochrome = True 

1148 elif np.iterable(colors): 

1149 if len(colors) == 1: 

1150 self.monochrome = True 

1151 self.colors = list( 

1152 itertools.islice(itertools.cycle(colors), N)) 

1153 else: 

1154 try: 

1155 gray = float(colors) 

1156 except TypeError: 

1157 pass 

1158 else: 

1159 self.colors = [gray] * N 

1160 self.monochrome = True 

1161 super().__init__(name, N) 

1162 

1163 def _init(self): 

1164 self._lut = np.zeros((self.N + 3, 4), float) 

1165 self._lut[:-3] = to_rgba_array(self.colors) 

1166 self._isinit = True 

1167 self._set_extremes() 

1168 

1169 def resampled(self, lutsize): 

1170 """Return a new colormap with *lutsize* entries.""" 

1171 colors = self(np.linspace(0, 1, lutsize)) 

1172 new_cmap = ListedColormap(colors, name=self.name) 

1173 # Keep the over/under values too 

1174 new_cmap._rgba_over = self._rgba_over 

1175 new_cmap._rgba_under = self._rgba_under 

1176 new_cmap._rgba_bad = self._rgba_bad 

1177 return new_cmap 

1178 

1179 def reversed(self, name=None): 

1180 """ 

1181 Return a reversed instance of the Colormap. 

1182 

1183 Parameters 

1184 ---------- 

1185 name : str, optional 

1186 The name for the reversed colormap. If None, the 

1187 name is set to ``self.name + "_r"``. 

1188 

1189 Returns 

1190 ------- 

1191 ListedColormap 

1192 A reversed instance of the colormap. 

1193 """ 

1194 if name is None: 

1195 name = self.name + "_r" 

1196 

1197 colors_r = list(reversed(self.colors)) 

1198 new_cmap = ListedColormap(colors_r, name=name, N=self.N) 

1199 # Reverse the over/under values too 

1200 new_cmap._rgba_over = self._rgba_under 

1201 new_cmap._rgba_under = self._rgba_over 

1202 new_cmap._rgba_bad = self._rgba_bad 

1203 return new_cmap 

1204 

1205 

1206class Normalize: 

1207 """ 

1208 A class which, when called, linearly normalizes data into the 

1209 ``[0.0, 1.0]`` interval. 

1210 """ 

1211 

1212 def __init__(self, vmin=None, vmax=None, clip=False): 

1213 """ 

1214 Parameters 

1215 ---------- 

1216 vmin, vmax : float or None 

1217 If *vmin* and/or *vmax* is not given, they are initialized from the 

1218 minimum and maximum value, respectively, of the first input 

1219 processed; i.e., ``__call__(A)`` calls ``autoscale_None(A)``. 

1220 

1221 clip : bool, default: False 

1222 If ``True`` values falling outside the range ``[vmin, vmax]``, 

1223 are mapped to 0 or 1, whichever is closer, and masked values are 

1224 set to 1. If ``False`` masked values remain masked. 

1225 

1226 Clipping silently defeats the purpose of setting the over, under, 

1227 and masked colors in a colormap, so it is likely to lead to 

1228 surprises; therefore the default is ``clip=False``. 

1229 

1230 Notes 

1231 ----- 

1232 Returns 0 if ``vmin == vmax``. 

1233 """ 

1234 self._vmin = _sanitize_extrema(vmin) 

1235 self._vmax = _sanitize_extrema(vmax) 

1236 self._clip = clip 

1237 self._scale = None 

1238 self.callbacks = cbook.CallbackRegistry(signals=["changed"]) 

1239 

1240 @property 

1241 def vmin(self): 

1242 return self._vmin 

1243 

1244 @vmin.setter 

1245 def vmin(self, value): 

1246 value = _sanitize_extrema(value) 

1247 if value != self._vmin: 

1248 self._vmin = value 

1249 self._changed() 

1250 

1251 @property 

1252 def vmax(self): 

1253 return self._vmax 

1254 

1255 @vmax.setter 

1256 def vmax(self, value): 

1257 value = _sanitize_extrema(value) 

1258 if value != self._vmax: 

1259 self._vmax = value 

1260 self._changed() 

1261 

1262 @property 

1263 def clip(self): 

1264 return self._clip 

1265 

1266 @clip.setter 

1267 def clip(self, value): 

1268 if value != self._clip: 

1269 self._clip = value 

1270 self._changed() 

1271 

1272 def _changed(self): 

1273 """ 

1274 Call this whenever the norm is changed to notify all the 

1275 callback listeners to the 'changed' signal. 

1276 """ 

1277 self.callbacks.process('changed') 

1278 

1279 @staticmethod 

1280 def process_value(value): 

1281 """ 

1282 Homogenize the input *value* for easy and efficient normalization. 

1283 

1284 *value* can be a scalar or sequence. 

1285 

1286 Returns 

1287 ------- 

1288 result : masked array 

1289 Masked array with the same shape as *value*. 

1290 is_scalar : bool 

1291 Whether *value* is a scalar. 

1292 

1293 Notes 

1294 ----- 

1295 Float dtypes are preserved; integer types with two bytes or smaller are 

1296 converted to np.float32, and larger types are converted to np.float64. 

1297 Preserving float32 when possible, and using in-place operations, 

1298 greatly improves speed for large arrays. 

1299 """ 

1300 is_scalar = not np.iterable(value) 

1301 if is_scalar: 

1302 value = [value] 

1303 dtype = np.min_scalar_type(value) 

1304 if np.issubdtype(dtype, np.integer) or dtype.type is np.bool_: 

1305 # bool_/int8/int16 -> float32; int32/int64 -> float64 

1306 dtype = np.promote_types(dtype, np.float32) 

1307 # ensure data passed in as an ndarray subclass are interpreted as 

1308 # an ndarray. See issue #6622. 

1309 mask = np.ma.getmask(value) 

1310 data = np.asarray(value) 

1311 result = np.ma.array(data, mask=mask, dtype=dtype, copy=True) 

1312 return result, is_scalar 

1313 

1314 def __call__(self, value, clip=None): 

1315 """ 

1316 Normalize *value* data in the ``[vmin, vmax]`` interval into the 

1317 ``[0.0, 1.0]`` interval and return it. 

1318 

1319 Parameters 

1320 ---------- 

1321 value 

1322 Data to normalize. 

1323 clip : bool 

1324 If ``None``, defaults to ``self.clip`` (which defaults to 

1325 ``False``). 

1326 

1327 Notes 

1328 ----- 

1329 If not already initialized, ``self.vmin`` and ``self.vmax`` are 

1330 initialized using ``self.autoscale_None(value)``. 

1331 """ 

1332 if clip is None: 

1333 clip = self.clip 

1334 

1335 result, is_scalar = self.process_value(value) 

1336 

1337 if self.vmin is None or self.vmax is None: 

1338 self.autoscale_None(result) 

1339 # Convert at least to float, without losing precision. 

1340 (vmin,), _ = self.process_value(self.vmin) 

1341 (vmax,), _ = self.process_value(self.vmax) 

1342 if vmin == vmax: 

1343 result.fill(0) # Or should it be all masked? Or 0.5? 

1344 elif vmin > vmax: 

1345 raise ValueError("minvalue must be less than or equal to maxvalue") 

1346 else: 

1347 if clip: 

1348 mask = np.ma.getmask(result) 

1349 result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax), 

1350 mask=mask) 

1351 # ma division is very slow; we can take a shortcut 

1352 resdat = result.data 

1353 resdat -= vmin 

1354 resdat /= (vmax - vmin) 

1355 result = np.ma.array(resdat, mask=result.mask, copy=False) 

1356 if is_scalar: 

1357 result = result[0] 

1358 return result 

1359 

1360 def inverse(self, value): 

1361 if not self.scaled(): 

1362 raise ValueError("Not invertible until both vmin and vmax are set") 

1363 (vmin,), _ = self.process_value(self.vmin) 

1364 (vmax,), _ = self.process_value(self.vmax) 

1365 

1366 if np.iterable(value): 

1367 val = np.ma.asarray(value) 

1368 return vmin + val * (vmax - vmin) 

1369 else: 

1370 return vmin + value * (vmax - vmin) 

1371 

1372 def autoscale(self, A): 

1373 """Set *vmin*, *vmax* to min, max of *A*.""" 

1374 self.vmin = self.vmax = None 

1375 self.autoscale_None(A) 

1376 

1377 def autoscale_None(self, A): 

1378 """If vmin or vmax are not set, use the min/max of *A* to set them.""" 

1379 A = np.asanyarray(A) 

1380 if self.vmin is None and A.size: 

1381 self.vmin = A.min() 

1382 if self.vmax is None and A.size: 

1383 self.vmax = A.max() 

1384 

1385 def scaled(self): 

1386 """Return whether vmin and vmax are set.""" 

1387 return self.vmin is not None and self.vmax is not None 

1388 

1389 

1390class TwoSlopeNorm(Normalize): 

1391 def __init__(self, vcenter, vmin=None, vmax=None): 

1392 """ 

1393 Normalize data with a set center. 

1394 

1395 Useful when mapping data with an unequal rates of change around a 

1396 conceptual center, e.g., data that range from -2 to 4, with 0 as 

1397 the midpoint. 

1398 

1399 Parameters 

1400 ---------- 

1401 vcenter : float 

1402 The data value that defines ``0.5`` in the normalization. 

1403 vmin : float, optional 

1404 The data value that defines ``0.0`` in the normalization. 

1405 Defaults to the min value of the dataset. 

1406 vmax : float, optional 

1407 The data value that defines ``1.0`` in the normalization. 

1408 Defaults to the max value of the dataset. 

1409 

1410 Examples 

1411 -------- 

1412 This maps data value -4000 to 0., 0 to 0.5, and +10000 to 1.0; data 

1413 between is linearly interpolated:: 

1414 

1415 >>> import matplotlib.colors as mcolors 

1416 >>> offset = mcolors.TwoSlopeNorm(vmin=-4000., 

1417 vcenter=0., vmax=10000) 

1418 >>> data = [-4000., -2000., 0., 2500., 5000., 7500., 10000.] 

1419 >>> offset(data) 

1420 array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0]) 

1421 """ 

1422 

1423 super().__init__(vmin=vmin, vmax=vmax) 

1424 self._vcenter = vcenter 

1425 if vcenter is not None and vmax is not None and vcenter >= vmax: 

1426 raise ValueError('vmin, vcenter, and vmax must be in ' 

1427 'ascending order') 

1428 if vcenter is not None and vmin is not None and vcenter <= vmin: 

1429 raise ValueError('vmin, vcenter, and vmax must be in ' 

1430 'ascending order') 

1431 

1432 @property 

1433 def vcenter(self): 

1434 return self._vcenter 

1435 

1436 @vcenter.setter 

1437 def vcenter(self, value): 

1438 if value != self._vcenter: 

1439 self._vcenter = value 

1440 self._changed() 

1441 

1442 def autoscale_None(self, A): 

1443 """ 

1444 Get vmin and vmax, and then clip at vcenter 

1445 """ 

1446 super().autoscale_None(A) 

1447 if self.vmin > self.vcenter: 

1448 self.vmin = self.vcenter 

1449 if self.vmax < self.vcenter: 

1450 self.vmax = self.vcenter 

1451 

1452 def __call__(self, value, clip=None): 

1453 """ 

1454 Map value to the interval [0, 1]. The clip argument is unused. 

1455 """ 

1456 result, is_scalar = self.process_value(value) 

1457 self.autoscale_None(result) # sets self.vmin, self.vmax if None 

1458 

1459 if not self.vmin <= self.vcenter <= self.vmax: 

1460 raise ValueError("vmin, vcenter, vmax must increase monotonically") 

1461 # note that we must extrapolate for tick locators: 

1462 result = np.ma.masked_array( 

1463 np.interp(result, [self.vmin, self.vcenter, self.vmax], 

1464 [0, 0.5, 1], left=-np.inf, right=np.inf), 

1465 mask=np.ma.getmask(result)) 

1466 if is_scalar: 

1467 result = np.atleast_1d(result)[0] 

1468 return result 

1469 

1470 def inverse(self, value): 

1471 if not self.scaled(): 

1472 raise ValueError("Not invertible until both vmin and vmax are set") 

1473 (vmin,), _ = self.process_value(self.vmin) 

1474 (vmax,), _ = self.process_value(self.vmax) 

1475 (vcenter,), _ = self.process_value(self.vcenter) 

1476 result = np.interp(value, [0, 0.5, 1], [vmin, vcenter, vmax], 

1477 left=-np.inf, right=np.inf) 

1478 return result 

1479 

1480 

1481class CenteredNorm(Normalize): 

1482 def __init__(self, vcenter=0, halfrange=None, clip=False): 

1483 """ 

1484 Normalize symmetrical data around a center (0 by default). 

1485 

1486 Unlike `TwoSlopeNorm`, `CenteredNorm` applies an equal rate of change 

1487 around the center. 

1488 

1489 Useful when mapping symmetrical data around a conceptual center 

1490 e.g., data that range from -2 to 4, with 0 as the midpoint, and 

1491 with equal rates of change around that midpoint. 

1492 

1493 Parameters 

1494 ---------- 

1495 vcenter : float, default: 0 

1496 The data value that defines ``0.5`` in the normalization. 

1497 halfrange : float, optional 

1498 The range of data values that defines a range of ``0.5`` in the 

1499 normalization, so that *vcenter* - *halfrange* is ``0.0`` and 

1500 *vcenter* + *halfrange* is ``1.0`` in the normalization. 

1501 Defaults to the largest absolute difference to *vcenter* for 

1502 the values in the dataset. 

1503 

1504 Examples 

1505 -------- 

1506 This maps data values -2 to 0.25, 0 to 0.5, and 4 to 1.0 

1507 (assuming equal rates of change above and below 0.0): 

1508 

1509 >>> import matplotlib.colors as mcolors 

1510 >>> norm = mcolors.CenteredNorm(halfrange=4.0) 

1511 >>> data = [-2., 0., 4.] 

1512 >>> norm(data) 

1513 array([0.25, 0.5 , 1. ]) 

1514 """ 

1515 super().__init__(vmin=None, vmax=None, clip=clip) 

1516 self._vcenter = vcenter 

1517 # calling the halfrange setter to set vmin and vmax 

1518 self.halfrange = halfrange 

1519 

1520 def _set_vmin_vmax(self): 

1521 """ 

1522 Set *vmin* and *vmax* based on *vcenter* and *halfrange*. 

1523 """ 

1524 self.vmax = self._vcenter + self._halfrange 

1525 self.vmin = self._vcenter - self._halfrange 

1526 

1527 def autoscale(self, A): 

1528 """ 

1529 Set *halfrange* to ``max(abs(A-vcenter))``, then set *vmin* and *vmax*. 

1530 """ 

1531 A = np.asanyarray(A) 

1532 self._halfrange = max(self._vcenter-A.min(), 

1533 A.max()-self._vcenter) 

1534 self._set_vmin_vmax() 

1535 

1536 def autoscale_None(self, A): 

1537 """Set *vmin* and *vmax*.""" 

1538 A = np.asanyarray(A) 

1539 if self._halfrange is None and A.size: 

1540 self.autoscale(A) 

1541 

1542 @property 

1543 def vcenter(self): 

1544 return self._vcenter 

1545 

1546 @vcenter.setter 

1547 def vcenter(self, vcenter): 

1548 if vcenter != self._vcenter: 

1549 self._vcenter = vcenter 

1550 self._changed() 

1551 if self.vmax is not None: 

1552 # recompute halfrange assuming vmin and vmax represent 

1553 # min and max of data 

1554 self._halfrange = max(self._vcenter-self.vmin, 

1555 self.vmax-self._vcenter) 

1556 self._set_vmin_vmax() 

1557 

1558 @property 

1559 def halfrange(self): 

1560 return self._halfrange 

1561 

1562 @halfrange.setter 

1563 def halfrange(self, halfrange): 

1564 if halfrange is None: 

1565 self._halfrange = None 

1566 self.vmin = None 

1567 self.vmax = None 

1568 else: 

1569 self._halfrange = abs(halfrange) 

1570 

1571 def __call__(self, value, clip=None): 

1572 if self._halfrange is not None: 

1573 # enforce symmetry, reset vmin and vmax 

1574 self._set_vmin_vmax() 

1575 return super().__call__(value, clip=clip) 

1576 

1577 

1578def make_norm_from_scale(scale_cls, base_norm_cls=None, *, init=None): 

1579 """ 

1580 Decorator for building a `.Normalize` subclass from a `~.scale.ScaleBase` 

1581 subclass. 

1582 

1583 After :: 

1584 

1585 @make_norm_from_scale(scale_cls) 

1586 class norm_cls(Normalize): 

1587 ... 

1588 

1589 *norm_cls* is filled with methods so that normalization computations are 

1590 forwarded to *scale_cls* (i.e., *scale_cls* is the scale that would be used 

1591 for the colorbar of a mappable normalized with *norm_cls*). 

1592 

1593 If *init* is not passed, then the constructor signature of *norm_cls* 

1594 will be ``norm_cls(vmin=None, vmax=None, clip=False)``; these three 

1595 parameters will be forwarded to the base class (``Normalize.__init__``), 

1596 and a *scale_cls* object will be initialized with no arguments (other than 

1597 a dummy axis). 

1598 

1599 If the *scale_cls* constructor takes additional parameters, then *init* 

1600 should be passed to `make_norm_from_scale`. It is a callable which is 

1601 *only* used for its signature. First, this signature will become the 

1602 signature of *norm_cls*. Second, the *norm_cls* constructor will bind the 

1603 parameters passed to it using this signature, extract the bound *vmin*, 

1604 *vmax*, and *clip* values, pass those to ``Normalize.__init__``, and 

1605 forward the remaining bound values (including any defaults defined by the 

1606 signature) to the *scale_cls* constructor. 

1607 """ 

1608 

1609 if base_norm_cls is None: 

1610 return functools.partial(make_norm_from_scale, scale_cls, init=init) 

1611 

1612 if isinstance(scale_cls, functools.partial): 

1613 scale_args = scale_cls.args 

1614 scale_kwargs_items = tuple(scale_cls.keywords.items()) 

1615 scale_cls = scale_cls.func 

1616 else: 

1617 scale_args = scale_kwargs_items = () 

1618 

1619 if init is None: 

1620 def init(vmin=None, vmax=None, clip=False): pass 

1621 

1622 return _make_norm_from_scale( 

1623 scale_cls, scale_args, scale_kwargs_items, 

1624 base_norm_cls, inspect.signature(init)) 

1625 

1626 

1627@functools.lru_cache(None) 

1628def _make_norm_from_scale( 

1629 scale_cls, scale_args, scale_kwargs_items, 

1630 base_norm_cls, bound_init_signature, 

1631): 

1632 """ 

1633 Helper for `make_norm_from_scale`. 

1634 

1635 This function is split out to enable caching (in particular so that 

1636 different unpickles reuse the same class). In order to do so, 

1637 

1638 - ``functools.partial`` *scale_cls* is expanded into ``func, args, kwargs`` 

1639 to allow memoizing returned norms (partial instances always compare 

1640 unequal, but we can check identity based on ``func, args, kwargs``; 

1641 - *init* is replaced by *init_signature*, as signatures are picklable, 

1642 unlike to arbitrary lambdas. 

1643 """ 

1644 

1645 class Norm(base_norm_cls): 

1646 def __reduce__(self): 

1647 cls = type(self) 

1648 # If the class is toplevel-accessible, it is possible to directly 

1649 # pickle it "by name". This is required to support norm classes 

1650 # defined at a module's toplevel, as the inner base_norm_cls is 

1651 # otherwise unpicklable (as it gets shadowed by the generated norm 

1652 # class). If either import or attribute access fails, fall back to 

1653 # the general path. 

1654 try: 

1655 if cls is getattr(importlib.import_module(cls.__module__), 

1656 cls.__qualname__): 

1657 return (_create_empty_object_of_class, (cls,), vars(self)) 

1658 except (ImportError, AttributeError): 

1659 pass 

1660 return (_picklable_norm_constructor, 

1661 (scale_cls, scale_args, scale_kwargs_items, 

1662 base_norm_cls, bound_init_signature), 

1663 vars(self)) 

1664 

1665 def __init__(self, *args, **kwargs): 

1666 ba = bound_init_signature.bind(*args, **kwargs) 

1667 ba.apply_defaults() 

1668 super().__init__( 

1669 **{k: ba.arguments.pop(k) for k in ["vmin", "vmax", "clip"]}) 

1670 self._scale = functools.partial( 

1671 scale_cls, *scale_args, **dict(scale_kwargs_items))( 

1672 axis=None, **ba.arguments) 

1673 self._trf = self._scale.get_transform() 

1674 

1675 __init__.__signature__ = bound_init_signature.replace(parameters=[ 

1676 inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD), 

1677 *bound_init_signature.parameters.values()]) 

1678 

1679 def __call__(self, value, clip=None): 

1680 value, is_scalar = self.process_value(value) 

1681 if self.vmin is None or self.vmax is None: 

1682 self.autoscale_None(value) 

1683 if self.vmin > self.vmax: 

1684 raise ValueError("vmin must be less or equal to vmax") 

1685 if self.vmin == self.vmax: 

1686 return np.full_like(value, 0) 

1687 if clip is None: 

1688 clip = self.clip 

1689 if clip: 

1690 value = np.clip(value, self.vmin, self.vmax) 

1691 t_value = self._trf.transform(value).reshape(np.shape(value)) 

1692 t_vmin, t_vmax = self._trf.transform([self.vmin, self.vmax]) 

1693 if not np.isfinite([t_vmin, t_vmax]).all(): 

1694 raise ValueError("Invalid vmin or vmax") 

1695 t_value -= t_vmin 

1696 t_value /= (t_vmax - t_vmin) 

1697 t_value = np.ma.masked_invalid(t_value, copy=False) 

1698 return t_value[0] if is_scalar else t_value 

1699 

1700 def inverse(self, value): 

1701 if not self.scaled(): 

1702 raise ValueError("Not invertible until scaled") 

1703 if self.vmin > self.vmax: 

1704 raise ValueError("vmin must be less or equal to vmax") 

1705 t_vmin, t_vmax = self._trf.transform([self.vmin, self.vmax]) 

1706 if not np.isfinite([t_vmin, t_vmax]).all(): 

1707 raise ValueError("Invalid vmin or vmax") 

1708 value, is_scalar = self.process_value(value) 

1709 rescaled = value * (t_vmax - t_vmin) 

1710 rescaled += t_vmin 

1711 value = (self._trf 

1712 .inverted() 

1713 .transform(rescaled) 

1714 .reshape(np.shape(value))) 

1715 return value[0] if is_scalar else value 

1716 

1717 def autoscale_None(self, A): 

1718 # i.e. A[np.isfinite(...)], but also for non-array A's 

1719 in_trf_domain = np.extract(np.isfinite(self._trf.transform(A)), A) 

1720 if in_trf_domain.size == 0: 

1721 in_trf_domain = np.ma.masked 

1722 return super().autoscale_None(in_trf_domain) 

1723 

1724 if base_norm_cls is Normalize: 

1725 Norm.__name__ = f"{scale_cls.__name__}Norm" 

1726 Norm.__qualname__ = f"{scale_cls.__qualname__}Norm" 

1727 else: 

1728 Norm.__name__ = base_norm_cls.__name__ 

1729 Norm.__qualname__ = base_norm_cls.__qualname__ 

1730 Norm.__module__ = base_norm_cls.__module__ 

1731 Norm.__doc__ = base_norm_cls.__doc__ 

1732 

1733 return Norm 

1734 

1735 

1736def _create_empty_object_of_class(cls): 

1737 return cls.__new__(cls) 

1738 

1739 

1740def _picklable_norm_constructor(*args): 

1741 return _create_empty_object_of_class(_make_norm_from_scale(*args)) 

1742 

1743 

1744@make_norm_from_scale( 

1745 scale.FuncScale, 

1746 init=lambda functions, vmin=None, vmax=None, clip=False: None) 

1747class FuncNorm(Normalize): 

1748 """ 

1749 Arbitrary normalization using functions for the forward and inverse. 

1750 

1751 Parameters 

1752 ---------- 

1753 functions : (callable, callable) 

1754 two-tuple of the forward and inverse functions for the normalization. 

1755 The forward function must be monotonic. 

1756 

1757 Both functions must have the signature :: 

1758 

1759 def forward(values: array-like) -> array-like 

1760 

1761 vmin, vmax : float or None 

1762 If *vmin* and/or *vmax* is not given, they are initialized from the 

1763 minimum and maximum value, respectively, of the first input 

1764 processed; i.e., ``__call__(A)`` calls ``autoscale_None(A)``. 

1765 

1766 clip : bool, default: False 

1767 If ``True`` values falling outside the range ``[vmin, vmax]``, 

1768 are mapped to 0 or 1, whichever is closer, and masked values are 

1769 set to 1. If ``False`` masked values remain masked. 

1770 

1771 Clipping silently defeats the purpose of setting the over, under, 

1772 and masked colors in a colormap, so it is likely to lead to 

1773 surprises; therefore the default is ``clip=False``. 

1774 """ 

1775 

1776 

1777LogNorm = make_norm_from_scale( 

1778 functools.partial(scale.LogScale, nonpositive="mask"))(Normalize) 

1779LogNorm.__name__ = LogNorm.__qualname__ = "LogNorm" 

1780LogNorm.__doc__ = "Normalize a given value to the 0-1 range on a log scale." 

1781 

1782 

1783@make_norm_from_scale( 

1784 scale.SymmetricalLogScale, 

1785 init=lambda linthresh, linscale=1., vmin=None, vmax=None, clip=False, *, 

1786 base=10: None) 

1787class SymLogNorm(Normalize): 

1788 """ 

1789 The symmetrical logarithmic scale is logarithmic in both the 

1790 positive and negative directions from the origin. 

1791 

1792 Since the values close to zero tend toward infinity, there is a 

1793 need to have a range around zero that is linear. The parameter 

1794 *linthresh* allows the user to specify the size of this range 

1795 (-*linthresh*, *linthresh*). 

1796 

1797 Parameters 

1798 ---------- 

1799 linthresh : float 

1800 The range within which the plot is linear (to avoid having the plot 

1801 go to infinity around zero). 

1802 linscale : float, default: 1 

1803 This allows the linear range (-*linthresh* to *linthresh*) to be 

1804 stretched relative to the logarithmic range. Its value is the 

1805 number of decades to use for each half of the linear range. For 

1806 example, when *linscale* == 1.0 (the default), the space used for 

1807 the positive and negative halves of the linear range will be equal 

1808 to one decade in the logarithmic range. 

1809 base : float, default: 10 

1810 """ 

1811 

1812 @property 

1813 def linthresh(self): 

1814 return self._scale.linthresh 

1815 

1816 @linthresh.setter 

1817 def linthresh(self, value): 

1818 self._scale.linthresh = value 

1819 

1820 

1821@make_norm_from_scale( 

1822 scale.AsinhScale, 

1823 init=lambda linear_width=1, vmin=None, vmax=None, clip=False: None) 

1824class AsinhNorm(Normalize): 

1825 """ 

1826 The inverse hyperbolic sine scale is approximately linear near 

1827 the origin, but becomes logarithmic for larger positive 

1828 or negative values. Unlike the `SymLogNorm`, the transition between 

1829 these linear and logarithmic regions is smooth, which may reduce 

1830 the risk of visual artifacts. 

1831 

1832 .. note:: 

1833 

1834 This API is provisional and may be revised in the future 

1835 based on early user feedback. 

1836 

1837 Parameters 

1838 ---------- 

1839 linear_width : float, default: 1 

1840 The effective width of the linear region, beyond which 

1841 the transformation becomes asymptotically logarithmic 

1842 """ 

1843 

1844 @property 

1845 def linear_width(self): 

1846 return self._scale.linear_width 

1847 

1848 @linear_width.setter 

1849 def linear_width(self, value): 

1850 self._scale.linear_width = value 

1851 

1852 

1853class PowerNorm(Normalize): 

1854 """ 

1855 Linearly map a given value to the 0-1 range and then apply 

1856 a power-law normalization over that range. 

1857 """ 

1858 def __init__(self, gamma, vmin=None, vmax=None, clip=False): 

1859 super().__init__(vmin, vmax, clip) 

1860 self.gamma = gamma 

1861 

1862 def __call__(self, value, clip=None): 

1863 if clip is None: 

1864 clip = self.clip 

1865 

1866 result, is_scalar = self.process_value(value) 

1867 

1868 self.autoscale_None(result) 

1869 gamma = self.gamma 

1870 vmin, vmax = self.vmin, self.vmax 

1871 if vmin > vmax: 

1872 raise ValueError("minvalue must be less than or equal to maxvalue") 

1873 elif vmin == vmax: 

1874 result.fill(0) 

1875 else: 

1876 if clip: 

1877 mask = np.ma.getmask(result) 

1878 result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax), 

1879 mask=mask) 

1880 resdat = result.data 

1881 resdat -= vmin 

1882 resdat[resdat < 0] = 0 

1883 np.power(resdat, gamma, resdat) 

1884 resdat /= (vmax - vmin) ** gamma 

1885 

1886 result = np.ma.array(resdat, mask=result.mask, copy=False) 

1887 if is_scalar: 

1888 result = result[0] 

1889 return result 

1890 

1891 def inverse(self, value): 

1892 if not self.scaled(): 

1893 raise ValueError("Not invertible until scaled") 

1894 gamma = self.gamma 

1895 vmin, vmax = self.vmin, self.vmax 

1896 

1897 if np.iterable(value): 

1898 val = np.ma.asarray(value) 

1899 return np.ma.power(val, 1. / gamma) * (vmax - vmin) + vmin 

1900 else: 

1901 return pow(value, 1. / gamma) * (vmax - vmin) + vmin 

1902 

1903 

1904class BoundaryNorm(Normalize): 

1905 """ 

1906 Generate a colormap index based on discrete intervals. 

1907 

1908 Unlike `Normalize` or `LogNorm`, `BoundaryNorm` maps values to integers 

1909 instead of to the interval 0-1. 

1910 """ 

1911 

1912 # Mapping to the 0-1 interval could have been done via piece-wise linear 

1913 # interpolation, but using integers seems simpler, and reduces the number 

1914 # of conversions back and forth between int and float. 

1915 

1916 def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): 

1917 """ 

1918 Parameters 

1919 ---------- 

1920 boundaries : array-like 

1921 Monotonically increasing sequence of at least 2 bin edges: data 

1922 falling in the n-th bin will be mapped to the n-th color. 

1923 

1924 ncolors : int 

1925 Number of colors in the colormap to be used. 

1926 

1927 clip : bool, optional 

1928 If clip is ``True``, out of range values are mapped to 0 if they 

1929 are below ``boundaries[0]`` or mapped to ``ncolors - 1`` if they 

1930 are above ``boundaries[-1]``. 

1931 

1932 If clip is ``False``, out of range values are mapped to -1 if 

1933 they are below ``boundaries[0]`` or mapped to *ncolors* if they are 

1934 above ``boundaries[-1]``. These are then converted to valid indices 

1935 by `Colormap.__call__`. 

1936 

1937 extend : {'neither', 'both', 'min', 'max'}, default: 'neither' 

1938 Extend the number of bins to include one or both of the 

1939 regions beyond the boundaries. For example, if ``extend`` 

1940 is 'min', then the color to which the region between the first 

1941 pair of boundaries is mapped will be distinct from the first 

1942 color in the colormap, and by default a 

1943 `~matplotlib.colorbar.Colorbar` will be drawn with 

1944 the triangle extension on the left or lower end. 

1945 

1946 Notes 

1947 ----- 

1948 If there are fewer bins (including extensions) than colors, then the 

1949 color index is chosen by linearly interpolating the ``[0, nbins - 1]`` 

1950 range onto the ``[0, ncolors - 1]`` range, effectively skipping some 

1951 colors in the middle of the colormap. 

1952 """ 

1953 if clip and extend != 'neither': 

1954 raise ValueError("'clip=True' is not compatible with 'extend'") 

1955 super().__init__(vmin=boundaries[0], vmax=boundaries[-1], clip=clip) 

1956 self.boundaries = np.asarray(boundaries) 

1957 self.N = len(self.boundaries) 

1958 if self.N < 2: 

1959 raise ValueError("You must provide at least 2 boundaries " 

1960 f"(1 region) but you passed in {boundaries!r}") 

1961 self.Ncmap = ncolors 

1962 self.extend = extend 

1963 

1964 self._scale = None # don't use the default scale. 

1965 

1966 self._n_regions = self.N - 1 # number of colors needed 

1967 self._offset = 0 

1968 if extend in ('min', 'both'): 

1969 self._n_regions += 1 

1970 self._offset = 1 

1971 if extend in ('max', 'both'): 

1972 self._n_regions += 1 

1973 if self._n_regions > self.Ncmap: 

1974 raise ValueError(f"There are {self._n_regions} color bins " 

1975 "including extensions, but ncolors = " 

1976 f"{ncolors}; ncolors must equal or exceed the " 

1977 "number of bins") 

1978 

1979 def __call__(self, value, clip=None): 

1980 """ 

1981 This method behaves similarly to `.Normalize.__call__`, except that it 

1982 returns integers or arrays of int16. 

1983 """ 

1984 if clip is None: 

1985 clip = self.clip 

1986 

1987 xx, is_scalar = self.process_value(value) 

1988 mask = np.ma.getmaskarray(xx) 

1989 # Fill masked values a value above the upper boundary 

1990 xx = np.atleast_1d(xx.filled(self.vmax + 1)) 

1991 if clip: 

1992 np.clip(xx, self.vmin, self.vmax, out=xx) 

1993 max_col = self.Ncmap - 1 

1994 else: 

1995 max_col = self.Ncmap 

1996 # this gives us the bins in the lookup table in the range 

1997 # [0, _n_regions - 1] (the offset is set in the init) 

1998 iret = np.digitize(xx, self.boundaries) - 1 + self._offset 

1999 # if we have more colors than regions, stretch the region 

2000 # index computed above to full range of the color bins. This 

2001 # will make use of the full range (but skip some of the colors 

2002 # in the middle) such that the first region is mapped to the 

2003 # first color and the last region is mapped to the last color. 

2004 if self.Ncmap > self._n_regions: 

2005 if self._n_regions == 1: 

2006 # special case the 1 region case, pick the middle color 

2007 iret[iret == 0] = (self.Ncmap - 1) // 2 

2008 else: 

2009 # otherwise linearly remap the values from the region index 

2010 # to the color index spaces 

2011 iret = (self.Ncmap - 1) / (self._n_regions - 1) * iret 

2012 # cast to 16bit integers in all cases 

2013 iret = iret.astype(np.int16) 

2014 iret[xx < self.vmin] = -1 

2015 iret[xx >= self.vmax] = max_col 

2016 ret = np.ma.array(iret, mask=mask) 

2017 if is_scalar: 

2018 ret = int(ret[0]) # assume python scalar 

2019 return ret 

2020 

2021 def inverse(self, value): 

2022 """ 

2023 Raises 

2024 ------ 

2025 ValueError 

2026 BoundaryNorm is not invertible, so calling this method will always 

2027 raise an error 

2028 """ 

2029 raise ValueError("BoundaryNorm is not invertible") 

2030 

2031 

2032class NoNorm(Normalize): 

2033 """ 

2034 Dummy replacement for `Normalize`, for the case where we want to use 

2035 indices directly in a `~matplotlib.cm.ScalarMappable`. 

2036 """ 

2037 def __call__(self, value, clip=None): 

2038 return value 

2039 

2040 def inverse(self, value): 

2041 return value 

2042 

2043 

2044def rgb_to_hsv(arr): 

2045 """ 

2046 Convert float RGB values (in the range [0, 1]), in a numpy array to HSV 

2047 values. 

2048 

2049 Parameters 

2050 ---------- 

2051 arr : (..., 3) array-like 

2052 All values must be in the range [0, 1] 

2053 

2054 Returns 

2055 ------- 

2056 (..., 3) ndarray 

2057 Colors converted to HSV values in range [0, 1] 

2058 """ 

2059 arr = np.asarray(arr) 

2060 

2061 # check length of the last dimension, should be _some_ sort of rgb 

2062 if arr.shape[-1] != 3: 

2063 raise ValueError("Last dimension of input array must be 3; " 

2064 "shape {} was found.".format(arr.shape)) 

2065 

2066 in_shape = arr.shape 

2067 arr = np.array( 

2068 arr, copy=False, 

2069 dtype=np.promote_types(arr.dtype, np.float32), # Don't work on ints. 

2070 ndmin=2, # In case input was 1D. 

2071 ) 

2072 out = np.zeros_like(arr) 

2073 arr_max = arr.max(-1) 

2074 ipos = arr_max > 0 

2075 delta = arr.ptp(-1) 

2076 s = np.zeros_like(delta) 

2077 s[ipos] = delta[ipos] / arr_max[ipos] 

2078 ipos = delta > 0 

2079 # red is max 

2080 idx = (arr[..., 0] == arr_max) & ipos 

2081 out[idx, 0] = (arr[idx, 1] - arr[idx, 2]) / delta[idx] 

2082 # green is max 

2083 idx = (arr[..., 1] == arr_max) & ipos 

2084 out[idx, 0] = 2. + (arr[idx, 2] - arr[idx, 0]) / delta[idx] 

2085 # blue is max 

2086 idx = (arr[..., 2] == arr_max) & ipos 

2087 out[idx, 0] = 4. + (arr[idx, 0] - arr[idx, 1]) / delta[idx] 

2088 

2089 out[..., 0] = (out[..., 0] / 6.0) % 1.0 

2090 out[..., 1] = s 

2091 out[..., 2] = arr_max 

2092 

2093 return out.reshape(in_shape) 

2094 

2095 

2096def hsv_to_rgb(hsv): 

2097 """ 

2098 Convert HSV values to RGB. 

2099 

2100 Parameters 

2101 ---------- 

2102 hsv : (..., 3) array-like 

2103 All values assumed to be in range [0, 1] 

2104 

2105 Returns 

2106 ------- 

2107 (..., 3) ndarray 

2108 Colors converted to RGB values in range [0, 1] 

2109 """ 

2110 hsv = np.asarray(hsv) 

2111 

2112 # check length of the last dimension, should be _some_ sort of rgb 

2113 if hsv.shape[-1] != 3: 

2114 raise ValueError("Last dimension of input array must be 3; " 

2115 "shape {shp} was found.".format(shp=hsv.shape)) 

2116 

2117 in_shape = hsv.shape 

2118 hsv = np.array( 

2119 hsv, copy=False, 

2120 dtype=np.promote_types(hsv.dtype, np.float32), # Don't work on ints. 

2121 ndmin=2, # In case input was 1D. 

2122 ) 

2123 

2124 h = hsv[..., 0] 

2125 s = hsv[..., 1] 

2126 v = hsv[..., 2] 

2127 

2128 r = np.empty_like(h) 

2129 g = np.empty_like(h) 

2130 b = np.empty_like(h) 

2131 

2132 i = (h * 6.0).astype(int) 

2133 f = (h * 6.0) - i 

2134 p = v * (1.0 - s) 

2135 q = v * (1.0 - s * f) 

2136 t = v * (1.0 - s * (1.0 - f)) 

2137 

2138 idx = i % 6 == 0 

2139 r[idx] = v[idx] 

2140 g[idx] = t[idx] 

2141 b[idx] = p[idx] 

2142 

2143 idx = i == 1 

2144 r[idx] = q[idx] 

2145 g[idx] = v[idx] 

2146 b[idx] = p[idx] 

2147 

2148 idx = i == 2 

2149 r[idx] = p[idx] 

2150 g[idx] = v[idx] 

2151 b[idx] = t[idx] 

2152 

2153 idx = i == 3 

2154 r[idx] = p[idx] 

2155 g[idx] = q[idx] 

2156 b[idx] = v[idx] 

2157 

2158 idx = i == 4 

2159 r[idx] = t[idx] 

2160 g[idx] = p[idx] 

2161 b[idx] = v[idx] 

2162 

2163 idx = i == 5 

2164 r[idx] = v[idx] 

2165 g[idx] = p[idx] 

2166 b[idx] = q[idx] 

2167 

2168 idx = s == 0 

2169 r[idx] = v[idx] 

2170 g[idx] = v[idx] 

2171 b[idx] = v[idx] 

2172 

2173 rgb = np.stack([r, g, b], axis=-1) 

2174 

2175 return rgb.reshape(in_shape) 

2176 

2177 

2178def _vector_magnitude(arr): 

2179 # things that don't work here: 

2180 # * np.linalg.norm: drops mask from ma.array 

2181 # * np.sum: drops mask from ma.array unless entire vector is masked 

2182 sum_sq = 0 

2183 for i in range(arr.shape[-1]): 

2184 sum_sq += arr[..., i, np.newaxis] ** 2 

2185 return np.sqrt(sum_sq) 

2186 

2187 

2188class LightSource: 

2189 """ 

2190 Create a light source coming from the specified azimuth and elevation. 

2191 Angles are in degrees, with the azimuth measured 

2192 clockwise from north and elevation up from the zero plane of the surface. 

2193 

2194 `shade` is used to produce "shaded" RGB values for a data array. 

2195 `shade_rgb` can be used to combine an RGB image with an elevation map. 

2196 `hillshade` produces an illumination map of a surface. 

2197 """ 

2198 

2199 def __init__(self, azdeg=315, altdeg=45, hsv_min_val=0, hsv_max_val=1, 

2200 hsv_min_sat=1, hsv_max_sat=0): 

2201 """ 

2202 Specify the azimuth (measured clockwise from south) and altitude 

2203 (measured up from the plane of the surface) of the light source 

2204 in degrees. 

2205 

2206 Parameters 

2207 ---------- 

2208 azdeg : float, default: 315 degrees (from the northwest) 

2209 The azimuth (0-360, degrees clockwise from North) of the light 

2210 source. 

2211 altdeg : float, default: 45 degrees 

2212 The altitude (0-90, degrees up from horizontal) of the light 

2213 source. 

2214 

2215 Notes 

2216 ----- 

2217 For backwards compatibility, the parameters *hsv_min_val*, 

2218 *hsv_max_val*, *hsv_min_sat*, and *hsv_max_sat* may be supplied at 

2219 initialization as well. However, these parameters will only be used if 

2220 "blend_mode='hsv'" is passed into `shade` or `shade_rgb`. 

2221 See the documentation for `blend_hsv` for more details. 

2222 """ 

2223 self.azdeg = azdeg 

2224 self.altdeg = altdeg 

2225 self.hsv_min_val = hsv_min_val 

2226 self.hsv_max_val = hsv_max_val 

2227 self.hsv_min_sat = hsv_min_sat 

2228 self.hsv_max_sat = hsv_max_sat 

2229 

2230 @property 

2231 def direction(self): 

2232 """The unit vector direction towards the light source.""" 

2233 # Azimuth is in degrees clockwise from North. Convert to radians 

2234 # counterclockwise from East (mathematical notation). 

2235 az = np.radians(90 - self.azdeg) 

2236 alt = np.radians(self.altdeg) 

2237 return np.array([ 

2238 np.cos(az) * np.cos(alt), 

2239 np.sin(az) * np.cos(alt), 

2240 np.sin(alt) 

2241 ]) 

2242 

2243 def hillshade(self, elevation, vert_exag=1, dx=1, dy=1, fraction=1.): 

2244 """ 

2245 Calculate the illumination intensity for a surface using the defined 

2246 azimuth and elevation for the light source. 

2247 

2248 This computes the normal vectors for the surface, and then passes them 

2249 on to `shade_normals` 

2250 

2251 Parameters 

2252 ---------- 

2253 elevation : 2D array-like 

2254 The height values used to generate an illumination map 

2255 vert_exag : number, optional 

2256 The amount to exaggerate the elevation values by when calculating 

2257 illumination. This can be used either to correct for differences in 

2258 units between the x-y coordinate system and the elevation 

2259 coordinate system (e.g. decimal degrees vs. meters) or to 

2260 exaggerate or de-emphasize topographic effects. 

2261 dx : number, optional 

2262 The x-spacing (columns) of the input *elevation* grid. 

2263 dy : number, optional 

2264 The y-spacing (rows) of the input *elevation* grid. 

2265 fraction : number, optional 

2266 Increases or decreases the contrast of the hillshade. Values 

2267 greater than one will cause intermediate values to move closer to 

2268 full illumination or shadow (and clipping any values that move 

2269 beyond 0 or 1). Note that this is not visually or mathematically 

2270 the same as vertical exaggeration. 

2271 

2272 Returns 

2273 ------- 

2274 ndarray 

2275 A 2D array of illumination values between 0-1, where 0 is 

2276 completely in shadow and 1 is completely illuminated. 

2277 """ 

2278 

2279 # Because most image and raster GIS data has the first row in the array 

2280 # as the "top" of the image, dy is implicitly negative. This is 

2281 # consistent to what `imshow` assumes, as well. 

2282 dy = -dy 

2283 

2284 # compute the normal vectors from the partial derivatives 

2285 e_dy, e_dx = np.gradient(vert_exag * elevation, dy, dx) 

2286 

2287 # .view is to keep subclasses 

2288 normal = np.empty(elevation.shape + (3,)).view(type(elevation)) 

2289 normal[..., 0] = -e_dx 

2290 normal[..., 1] = -e_dy 

2291 normal[..., 2] = 1 

2292 normal /= _vector_magnitude(normal) 

2293 

2294 return self.shade_normals(normal, fraction) 

2295 

2296 def shade_normals(self, normals, fraction=1.): 

2297 """ 

2298 Calculate the illumination intensity for the normal vectors of a 

2299 surface using the defined azimuth and elevation for the light source. 

2300 

2301 Imagine an artificial sun placed at infinity in some azimuth and 

2302 elevation position illuminating our surface. The parts of the surface 

2303 that slope toward the sun should brighten while those sides facing away 

2304 should become darker. 

2305 

2306 Parameters 

2307 ---------- 

2308 fraction : number, optional 

2309 Increases or decreases the contrast of the hillshade. Values 

2310 greater than one will cause intermediate values to move closer to 

2311 full illumination or shadow (and clipping any values that move 

2312 beyond 0 or 1). Note that this is not visually or mathematically 

2313 the same as vertical exaggeration. 

2314 

2315 Returns 

2316 ------- 

2317 ndarray 

2318 A 2D array of illumination values between 0-1, where 0 is 

2319 completely in shadow and 1 is completely illuminated. 

2320 """ 

2321 

2322 intensity = normals.dot(self.direction) 

2323 

2324 # Apply contrast stretch 

2325 imin, imax = intensity.min(), intensity.max() 

2326 intensity *= fraction 

2327 

2328 # Rescale to 0-1, keeping range before contrast stretch 

2329 # If constant slope, keep relative scaling (i.e. flat should be 0.5, 

2330 # fully occluded 0, etc.) 

2331 if (imax - imin) > 1e-6: 

2332 # Strictly speaking, this is incorrect. Negative values should be 

2333 # clipped to 0 because they're fully occluded. However, rescaling 

2334 # in this manner is consistent with the previous implementation and 

2335 # visually appears better than a "hard" clip. 

2336 intensity -= imin 

2337 intensity /= (imax - imin) 

2338 intensity = np.clip(intensity, 0, 1) 

2339 

2340 return intensity 

2341 

2342 def shade(self, data, cmap, norm=None, blend_mode='overlay', vmin=None, 

2343 vmax=None, vert_exag=1, dx=1, dy=1, fraction=1, **kwargs): 

2344 """ 

2345 Combine colormapped data values with an illumination intensity map 

2346 (a.k.a. "hillshade") of the values. 

2347 

2348 Parameters 

2349 ---------- 

2350 data : 2D array-like 

2351 The height values used to generate a shaded map. 

2352 cmap : `~matplotlib.colors.Colormap` 

2353 The colormap used to color the *data* array. Note that this must be 

2354 a `~matplotlib.colors.Colormap` instance. For example, rather than 

2355 passing in ``cmap='gist_earth'``, use 

2356 ``cmap=plt.get_cmap('gist_earth')`` instead. 

2357 norm : `~matplotlib.colors.Normalize` instance, optional 

2358 The normalization used to scale values before colormapping. If 

2359 None, the input will be linearly scaled between its min and max. 

2360 blend_mode : {'hsv', 'overlay', 'soft'} or callable, optional 

2361 The type of blending used to combine the colormapped data 

2362 values with the illumination intensity. Default is 

2363 "overlay". Note that for most topographic surfaces, 

2364 "overlay" or "soft" appear more visually realistic. If a 

2365 user-defined function is supplied, it is expected to 

2366 combine an MxNx3 RGB array of floats (ranging 0 to 1) with 

2367 an MxNx1 hillshade array (also 0 to 1). (Call signature 

2368 ``func(rgb, illum, **kwargs)``) Additional kwargs supplied 

2369 to this function will be passed on to the *blend_mode* 

2370 function. 

2371 vmin : float or None, optional 

2372 The minimum value used in colormapping *data*. If *None* the 

2373 minimum value in *data* is used. If *norm* is specified, then this 

2374 argument will be ignored. 

2375 vmax : float or None, optional 

2376 The maximum value used in colormapping *data*. If *None* the 

2377 maximum value in *data* is used. If *norm* is specified, then this 

2378 argument will be ignored. 

2379 vert_exag : number, optional 

2380 The amount to exaggerate the elevation values by when calculating 

2381 illumination. This can be used either to correct for differences in 

2382 units between the x-y coordinate system and the elevation 

2383 coordinate system (e.g. decimal degrees vs. meters) or to 

2384 exaggerate or de-emphasize topography. 

2385 dx : number, optional 

2386 The x-spacing (columns) of the input *elevation* grid. 

2387 dy : number, optional 

2388 The y-spacing (rows) of the input *elevation* grid. 

2389 fraction : number, optional 

2390 Increases or decreases the contrast of the hillshade. Values 

2391 greater than one will cause intermediate values to move closer to 

2392 full illumination or shadow (and clipping any values that move 

2393 beyond 0 or 1). Note that this is not visually or mathematically 

2394 the same as vertical exaggeration. 

2395 Additional kwargs are passed on to the *blend_mode* function. 

2396 

2397 Returns 

2398 ------- 

2399 ndarray 

2400 An MxNx4 array of floats ranging between 0-1. 

2401 """ 

2402 if vmin is None: 

2403 vmin = data.min() 

2404 if vmax is None: 

2405 vmax = data.max() 

2406 if norm is None: 

2407 norm = Normalize(vmin=vmin, vmax=vmax) 

2408 

2409 rgb0 = cmap(norm(data)) 

2410 rgb1 = self.shade_rgb(rgb0, elevation=data, blend_mode=blend_mode, 

2411 vert_exag=vert_exag, dx=dx, dy=dy, 

2412 fraction=fraction, **kwargs) 

2413 # Don't overwrite the alpha channel, if present. 

2414 rgb0[..., :3] = rgb1[..., :3] 

2415 return rgb0 

2416 

2417 def shade_rgb(self, rgb, elevation, fraction=1., blend_mode='hsv', 

2418 vert_exag=1, dx=1, dy=1, **kwargs): 

2419 """ 

2420 Use this light source to adjust the colors of the *rgb* input array to 

2421 give the impression of a shaded relief map with the given *elevation*. 

2422 

2423 Parameters 

2424 ---------- 

2425 rgb : array-like 

2426 An (M, N, 3) RGB array, assumed to be in the range of 0 to 1. 

2427 elevation : array-like 

2428 An (M, N) array of the height values used to generate a shaded map. 

2429 fraction : number 

2430 Increases or decreases the contrast of the hillshade. Values 

2431 greater than one will cause intermediate values to move closer to 

2432 full illumination or shadow (and clipping any values that move 

2433 beyond 0 or 1). Note that this is not visually or mathematically 

2434 the same as vertical exaggeration. 

2435 blend_mode : {'hsv', 'overlay', 'soft'} or callable, optional 

2436 The type of blending used to combine the colormapped data values 

2437 with the illumination intensity. For backwards compatibility, this 

2438 defaults to "hsv". Note that for most topographic surfaces, 

2439 "overlay" or "soft" appear more visually realistic. If a 

2440 user-defined function is supplied, it is expected to combine an 

2441 MxNx3 RGB array of floats (ranging 0 to 1) with an MxNx1 hillshade 

2442 array (also 0 to 1). (Call signature 

2443 ``func(rgb, illum, **kwargs)``) 

2444 Additional kwargs supplied to this function will be passed on to 

2445 the *blend_mode* function. 

2446 vert_exag : number, optional 

2447 The amount to exaggerate the elevation values by when calculating 

2448 illumination. This can be used either to correct for differences in 

2449 units between the x-y coordinate system and the elevation 

2450 coordinate system (e.g. decimal degrees vs. meters) or to 

2451 exaggerate or de-emphasize topography. 

2452 dx : number, optional 

2453 The x-spacing (columns) of the input *elevation* grid. 

2454 dy : number, optional 

2455 The y-spacing (rows) of the input *elevation* grid. 

2456 Additional kwargs are passed on to the *blend_mode* function. 

2457 

2458 Returns 

2459 ------- 

2460 ndarray 

2461 An (m, n, 3) array of floats ranging between 0-1. 

2462 """ 

2463 # Calculate the "hillshade" intensity. 

2464 intensity = self.hillshade(elevation, vert_exag, dx, dy, fraction) 

2465 intensity = intensity[..., np.newaxis] 

2466 

2467 # Blend the hillshade and rgb data using the specified mode 

2468 lookup = { 

2469 'hsv': self.blend_hsv, 

2470 'soft': self.blend_soft_light, 

2471 'overlay': self.blend_overlay, 

2472 } 

2473 if blend_mode in lookup: 

2474 blend = lookup[blend_mode](rgb, intensity, **kwargs) 

2475 else: 

2476 try: 

2477 blend = blend_mode(rgb, intensity, **kwargs) 

2478 except TypeError as err: 

2479 raise ValueError('"blend_mode" must be callable or one of {}' 

2480 .format(lookup.keys)) from err 

2481 

2482 # Only apply result where hillshade intensity isn't masked 

2483 if np.ma.is_masked(intensity): 

2484 mask = intensity.mask[..., 0] 

2485 for i in range(3): 

2486 blend[..., i][mask] = rgb[..., i][mask] 

2487 

2488 return blend 

2489 

2490 def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None, 

2491 hsv_min_val=None, hsv_min_sat=None): 

2492 """ 

2493 Take the input data array, convert to HSV values in the given colormap, 

2494 then adjust those color values to give the impression of a shaded 

2495 relief map with a specified light source. RGBA values are returned, 

2496 which can then be used to plot the shaded image with imshow. 

2497 

2498 The color of the resulting image will be darkened by moving the (s, v) 

2499 values (in HSV colorspace) toward (hsv_min_sat, hsv_min_val) in the 

2500 shaded regions, or lightened by sliding (s, v) toward (hsv_max_sat, 

2501 hsv_max_val) in regions that are illuminated. The default extremes are 

2502 chose so that completely shaded points are nearly black (s = 1, v = 0) 

2503 and completely illuminated points are nearly white (s = 0, v = 1). 

2504 

2505 Parameters 

2506 ---------- 

2507 rgb : ndarray 

2508 An MxNx3 RGB array of floats ranging from 0 to 1 (color image). 

2509 intensity : ndarray 

2510 An MxNx1 array of floats ranging from 0 to 1 (grayscale image). 

2511 hsv_max_sat : number, default: 1 

2512 The maximum saturation value that the *intensity* map can shift the 

2513 output image to. 

2514 hsv_min_sat : number, optional 

2515 The minimum saturation value that the *intensity* map can shift the 

2516 output image to. Defaults to 0. 

2517 hsv_max_val : number, optional 

2518 The maximum value ("v" in "hsv") that the *intensity* map can shift 

2519 the output image to. Defaults to 1. 

2520 hsv_min_val : number, optional 

2521 The minimum value ("v" in "hsv") that the *intensity* map can shift 

2522 the output image to. Defaults to 0. 

2523 

2524 Returns 

2525 ------- 

2526 ndarray 

2527 An MxNx3 RGB array representing the combined images. 

2528 """ 

2529 # Backward compatibility... 

2530 if hsv_max_sat is None: 

2531 hsv_max_sat = self.hsv_max_sat 

2532 if hsv_max_val is None: 

2533 hsv_max_val = self.hsv_max_val 

2534 if hsv_min_sat is None: 

2535 hsv_min_sat = self.hsv_min_sat 

2536 if hsv_min_val is None: 

2537 hsv_min_val = self.hsv_min_val 

2538 

2539 # Expects a 2D intensity array scaled between -1 to 1... 

2540 intensity = intensity[..., 0] 

2541 intensity = 2 * intensity - 1 

2542 

2543 # Convert to rgb, then rgb to hsv 

2544 hsv = rgb_to_hsv(rgb[:, :, 0:3]) 

2545 hue, sat, val = np.moveaxis(hsv, -1, 0) 

2546 

2547 # Modify hsv values (in place) to simulate illumination. 

2548 # putmask(A, mask, B) <=> A[mask] = B[mask] 

2549 np.putmask(sat, (np.abs(sat) > 1.e-10) & (intensity > 0), 

2550 (1 - intensity) * sat + intensity * hsv_max_sat) 

2551 np.putmask(sat, (np.abs(sat) > 1.e-10) & (intensity < 0), 

2552 (1 + intensity) * sat - intensity * hsv_min_sat) 

2553 np.putmask(val, intensity > 0, 

2554 (1 - intensity) * val + intensity * hsv_max_val) 

2555 np.putmask(val, intensity < 0, 

2556 (1 + intensity) * val - intensity * hsv_min_val) 

2557 np.clip(hsv[:, :, 1:], 0, 1, out=hsv[:, :, 1:]) 

2558 

2559 # Convert modified hsv back to rgb. 

2560 return hsv_to_rgb(hsv) 

2561 

2562 def blend_soft_light(self, rgb, intensity): 

2563 """ 

2564 Combine an RGB image with an intensity map using "soft light" blending, 

2565 using the "pegtop" formula. 

2566 

2567 Parameters 

2568 ---------- 

2569 rgb : ndarray 

2570 An MxNx3 RGB array of floats ranging from 0 to 1 (color image). 

2571 intensity : ndarray 

2572 An MxNx1 array of floats ranging from 0 to 1 (grayscale image). 

2573 

2574 Returns 

2575 ------- 

2576 ndarray 

2577 An MxNx3 RGB array representing the combined images. 

2578 """ 

2579 return 2 * intensity * rgb + (1 - 2 * intensity) * rgb**2 

2580 

2581 def blend_overlay(self, rgb, intensity): 

2582 """ 

2583 Combine an RGB image with an intensity map using "overlay" blending. 

2584 

2585 Parameters 

2586 ---------- 

2587 rgb : ndarray 

2588 An MxNx3 RGB array of floats ranging from 0 to 1 (color image). 

2589 intensity : ndarray 

2590 An MxNx1 array of floats ranging from 0 to 1 (grayscale image). 

2591 

2592 Returns 

2593 ------- 

2594 ndarray 

2595 An MxNx3 RGB array representing the combined images. 

2596 """ 

2597 low = 2 * intensity * rgb 

2598 high = 1 - 2 * (1 - intensity) * (1 - rgb) 

2599 return np.where(rgb <= 0.5, low, high) 

2600 

2601 

2602def from_levels_and_colors(levels, colors, extend='neither'): 

2603 """ 

2604 A helper routine to generate a cmap and a norm instance which 

2605 behave similar to contourf's levels and colors arguments. 

2606 

2607 Parameters 

2608 ---------- 

2609 levels : sequence of numbers 

2610 The quantization levels used to construct the `BoundaryNorm`. 

2611 Value ``v`` is quantized to level ``i`` if ``lev[i] <= v < lev[i+1]``. 

2612 colors : sequence of colors 

2613 The fill color to use for each level. If *extend* is "neither" there 

2614 must be ``n_level - 1`` colors. For an *extend* of "min" or "max" add 

2615 one extra color, and for an *extend* of "both" add two colors. 

2616 extend : {'neither', 'min', 'max', 'both'}, optional 

2617 The behaviour when a value falls out of range of the given levels. 

2618 See `~.Axes.contourf` for details. 

2619 

2620 Returns 

2621 ------- 

2622 cmap : `~matplotlib.colors.Normalize` 

2623 norm : `~matplotlib.colors.Colormap` 

2624 """ 

2625 slice_map = { 

2626 'both': slice(1, -1), 

2627 'min': slice(1, None), 

2628 'max': slice(0, -1), 

2629 'neither': slice(0, None), 

2630 } 

2631 _api.check_in_list(slice_map, extend=extend) 

2632 color_slice = slice_map[extend] 

2633 

2634 n_data_colors = len(levels) - 1 

2635 n_expected = n_data_colors + color_slice.start - (color_slice.stop or 0) 

2636 if len(colors) != n_expected: 

2637 raise ValueError( 

2638 f'With extend == {extend!r} and {len(levels)} levels, ' 

2639 f'expected {n_expected} colors, but got {len(colors)}') 

2640 

2641 cmap = ListedColormap(colors[color_slice], N=n_data_colors) 

2642 

2643 if extend in ['min', 'both']: 

2644 cmap.set_under(colors[0]) 

2645 else: 

2646 cmap.set_under('none') 

2647 

2648 if extend in ['max', 'both']: 

2649 cmap.set_over(colors[-1]) 

2650 else: 

2651 cmap.set_over('none') 

2652 

2653 cmap.colorbar_extend = extend 

2654 

2655 norm = BoundaryNorm(levels, ncolors=n_data_colors) 

2656 return cmap, norm