|
12 | 12 | import numpy as np
|
13 | 13 |
|
14 | 14 | import matplotlib as mpl
|
15 |
| -from . import _api, cbook |
16 |
| -from .colors import BoundaryNorm, BivarColormap |
17 |
| -from .cm import VectorMappable |
| 15 | +from . import _api, cbook, cm |
| 16 | +from .cm import ScalarMappable |
18 | 17 | from .path import Path
|
19 | 18 | from .transforms import (BboxBase, Bbox, IdentityTransform, Transform, TransformedBbox,
|
20 | 19 | TransformedPatchPath, TransformedPath)
|
@@ -1346,49 +1345,16 @@ def format_cursor_data(self, data):
|
1346 | 1345 | --------
|
1347 | 1346 | get_cursor_data
|
1348 | 1347 | """
|
1349 |
| - if isinstance(self, VectorMappable) and \ |
1350 |
| - (np.ndim(data) == 0 or len(data) == self.cmap.n_variates): |
1351 |
| - # This block logically belongs to VectorMappable, but can't be |
1352 |
| - # implemented in it because most VectorMappable subclasses |
1353 |
| - # inherit from Artist first and from VectorMappable second, so |
| 1348 | + if isinstance(self, ScalarMappable): |
| 1349 | + # Internal classes no longer inherit from ScalarMappable, and this |
| 1350 | + # block should never be executed by the internal API |
| 1351 | + |
| 1352 | + # This block logically belongs to ScalarMappable, but can't be |
| 1353 | + # implemented in it in case custom ScalarMappable subclasses |
| 1354 | + # inherit from Artist first and from ScalarMappable second, so |
1354 | 1355 | # Artist.format_cursor_data would always have precedence over
|
1355 |
| - # VectorMappable.format_cursor_data. |
1356 |
| - if self.cmap.n_variates == 1: |
1357 |
| - data = [data] |
1358 |
| - # The above if test is equivalent to `isinstance(self.cmap, Colormap)` |
1359 |
| - num_colors = [self.cmap.N] |
1360 |
| - else: |
1361 |
| - if isinstance(self.cmap, BivarColormap): |
1362 |
| - num_colors = [self.cmap.N, self.cmap.M] |
1363 |
| - else: # i.e. a MultivarColormap object |
1364 |
| - num_colors = [component.N for component in self.cmap] |
1365 |
| - |
1366 |
| - out_str = '[' |
1367 |
| - for nn, dd, nc in zip(self.nac._norm, data, num_colors): |
1368 |
| - if np.ma.getmask(dd): |
1369 |
| - out_str += ", " |
1370 |
| - else: |
1371 |
| - # Figure out a reasonable amount of significant digits |
1372 |
| - normed = nn(dd) |
1373 |
| - if np.isfinite(normed): |
1374 |
| - if isinstance(nn, BoundaryNorm): |
1375 |
| - # not an invertible normalization mapping |
1376 |
| - cur_idx = np.argmin(np.abs(nn.boundaries - dd)) |
1377 |
| - neigh_idx = max(0, cur_idx - 1) |
1378 |
| - # use max diff to prevent delta == 0 |
1379 |
| - delta = np.diff( |
1380 |
| - nn.boundaries[neigh_idx:cur_idx + 2] |
1381 |
| - ).max() |
1382 |
| - else: |
1383 |
| - # Midpoints of neighboring color intervals. |
1384 |
| - neighbors = nn.inverse( |
1385 |
| - (int(normed * nc) + np.array([0, 1])) / nc) |
1386 |
| - delta = abs(neighbors - dd).max() |
1387 |
| - g_sig_digits = cbook._g_sig_digits(dd, delta) |
1388 |
| - else: |
1389 |
| - g_sig_digits = 3 # Consistent with default below. |
1390 |
| - out_str += f"{dd:-#.{g_sig_digits}g}, " |
1391 |
| - return out_str[:-2] + ']' |
| 1356 | + # ScalarMappable.format_cursor_data. |
| 1357 | + return self.mapper._format_cursor_data(data) |
1392 | 1358 | else:
|
1393 | 1359 | try:
|
1394 | 1360 | data[0]
|
@@ -1431,6 +1397,115 @@ def set_mouseover(self, mouseover):
|
1431 | 1397 | mouseover = property(get_mouseover, set_mouseover) # backcompat.
|
1432 | 1398 |
|
1433 | 1399 |
|
| 1400 | +class ColorableArtist(Artist): |
| 1401 | + def __init__(self, norm=None, cmap=None): |
| 1402 | + """ |
| 1403 | + Parameters |
| 1404 | + ---------- |
| 1405 | + norm : `.Normalize` (or subclass thereof) or str or None |
| 1406 | + The normalizing object which scales data, typically into the |
| 1407 | + interval ``[0, 1]``. |
| 1408 | + If a `str`, a `.Normalize` subclass is dynamically generated based |
| 1409 | + on the scale with the corresponding name. |
| 1410 | + If *None*, *norm* defaults to a *colors.Normalize* object which |
| 1411 | + initializes its scaling based on the first data processed. |
| 1412 | + cmap : str or `~matplotlib.colors.Colormap` |
| 1413 | + The colormap used to map normalized data values to RGBA colors. |
| 1414 | + """ |
| 1415 | + |
| 1416 | + Artist.__init__(self) |
| 1417 | + |
| 1418 | + self._A = None |
| 1419 | + if isinstance(norm, cm.Mapper): |
| 1420 | + self._mapper = norm |
| 1421 | + else: |
| 1422 | + self._mapper = cm.Mapper(cmap, norm) |
| 1423 | + |
| 1424 | + self._id_mapper = self.mapper.callbacks.connect('changed', self.changed) |
| 1425 | + self.callbacks = cbook.CallbackRegistry(signals=["changed"]) |
| 1426 | + |
| 1427 | + def set_array(self, A): |
| 1428 | + """ |
| 1429 | + Set the value array from array-like *A*. |
| 1430 | +
|
| 1431 | + Parameters |
| 1432 | + ---------- |
| 1433 | + A : array-like or None |
| 1434 | + The values that are mapped to colors. |
| 1435 | +
|
| 1436 | + The base class `.VectorMappable` does not make any assumptions on |
| 1437 | + the dimensionality and shape of the value array *A*. |
| 1438 | + """ |
| 1439 | + if A is None: |
| 1440 | + self._A = None |
| 1441 | + return |
| 1442 | + A = cm._ensure_multivariate_data(self.cmap.n_variates, A) |
| 1443 | + |
| 1444 | + A = cbook.safe_masked_invalid(A, copy=True) |
| 1445 | + if not np.can_cast(A.dtype, float, "same_kind"): |
| 1446 | + if A.dtype.fields is None: |
| 1447 | + raise TypeError(f"Image data of dtype {A.dtype} cannot be " |
| 1448 | + f"converted to float") |
| 1449 | + else: |
| 1450 | + for key in A.dtype.fields: |
| 1451 | + if not np.can_cast(A[key].dtype, float, "same_kind"): |
| 1452 | + raise TypeError(f"Image data of dtype {A.dtype} cannot be " |
| 1453 | + f"converted to a sequence of floats") |
| 1454 | + self._A = A |
| 1455 | + self.mapper.autoscale_None(A) |
| 1456 | + |
| 1457 | + @property |
| 1458 | + def mapper(self): |
| 1459 | + return self._mapper |
| 1460 | + |
| 1461 | + @mapper.setter |
| 1462 | + def mapper(self, mapper): |
| 1463 | + self._set_mapper(mapper) |
| 1464 | + |
| 1465 | + def _set_mapper(self, mapper): |
| 1466 | + if isinstance(mapper, cm.Mapper): |
| 1467 | + if self._A is not None: |
| 1468 | + if not mapper.n_variates == self.mapper.n_variates: |
| 1469 | + raise ValueError('The new Mapper object must have the same' |
| 1470 | + ' number of variates as the existing data.') |
| 1471 | + else: |
| 1472 | + self.mapper.callbacks.disconnect(self._id_mapper) |
| 1473 | + self._mapper = mapper |
| 1474 | + self._id_mapper = mapper.callbacks.connect('changed', self.changed) |
| 1475 | + self.changed() |
| 1476 | + else: |
| 1477 | + raise ValueError('Only a Mapper object can be set to mapper.') |
| 1478 | + |
| 1479 | + def get_array(self): |
| 1480 | + """ |
| 1481 | + Return the array of values, that are mapped to colors. |
| 1482 | +
|
| 1483 | + The base class `.VectorMappable` does not make any assumptions on |
| 1484 | + the dimensionality and shape of the array. |
| 1485 | + """ |
| 1486 | + return self._A |
| 1487 | + |
| 1488 | + def changed(self): |
| 1489 | + """ |
| 1490 | + Call this whenever the mappable is changed to notify all the |
| 1491 | + callbackSM listeners to the 'changed' signal. |
| 1492 | + """ |
| 1493 | + self.callbacks.process('changed') |
| 1494 | + self.stale = True |
| 1495 | + |
| 1496 | + def format_cursor_data(self, data): |
| 1497 | + """ |
| 1498 | + Return a string representation of *data*. |
| 1499 | +
|
| 1500 | + Uses the colorbar's formatter to format the data. |
| 1501 | +
|
| 1502 | + See Also |
| 1503 | + -------- |
| 1504 | + get_cursor_data |
| 1505 | + """ |
| 1506 | + return self.mapper._format_cursor_data(data) |
| 1507 | + |
| 1508 | + |
1434 | 1509 | def _get_tightbbox_for_layout_only(obj, *args, **kwargs):
|
1435 | 1510 | """
|
1436 | 1511 | Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
|
|
0 commit comments