Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5a6c99b

Browse files
committed
ENH: Adding zoom and pan to colorbar
The zoom and pan funcitons change the vmin/vmax of the norm attached to the colorbar. The colorbar is rendered as an inset axis, but the event handler is implemented on the parent axis.
1 parent 464dcf6 commit 5a6c99b

File tree

2 files changed

+248
-0
lines changed

2 files changed

+248
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Colorbars now have pan and zoom functionality
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Interactive plots with colorbars can now be zoomed and panned on
5+
the colorbar axis. This adjusts the *vmin* and *vmax* of the
6+
``ScalarMappable`` associated with the colorbar.

lib/matplotlib/colorbar.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import copy
3232
import logging
3333
import textwrap
34+
import types
3435

3536
import numpy as np
3637

@@ -1199,6 +1200,11 @@ def __init__(self, ax, mappable, **kwargs):
11991200
mappable.colorbar_cid = mappable.callbacksSM.connect(
12001201
'changed', self.update_normal)
12011202

1203+
# Handle interactiveness on the outer axis
1204+
for x in ["_get_view", "_set_view", "_set_view_from_bbox",
1205+
"start_pan", "end_pan", "drag_pan"]:
1206+
setattr(self.ax.outer_ax, x, getattr(self, x))
1207+
12021208
@_api.deprecated("3.3", alternative="update_normal")
12031209
def on_mappable_changed(self, mappable):
12041210
"""
@@ -1326,6 +1332,242 @@ def remove(self):
13261332
# use_gridspec was True
13271333
ax.set_subplotspec(subplotspec)
13281334

1335+
def _get_view(self):
1336+
"""
1337+
Save information required to reproduce the current view.
1338+
1339+
The colorbar is controlled by the norm's vmin and vmax
1340+
"""
1341+
return self.norm.vmin, self.norm.vmax
1342+
1343+
def _set_view(self, view):
1344+
"""
1345+
Apply a previously saved view.
1346+
1347+
This sets the vmin and vmax of the norm associated with
1348+
the scalar mappable. We then update the normal to redraw
1349+
the content that was updated.
1350+
"""
1351+
self.mappable.norm.vmin, self.mappable.norm.vmax = view
1352+
self.update_normal(self.mappable)
1353+
1354+
def _set_view_from_bbox(self, bbox, direction='in',
1355+
mode=None, twinx=False, twiny=False):
1356+
"""
1357+
Update view from a selection bbox.
1358+
1359+
The view of a colorbar is controlled by the vmin and vmax of the
1360+
norm. Updating the view from a bbox will change the vmin and vmax
1361+
to the extent of the selection when zooming in and scale the values
1362+
away from the selection when zooming out.
1363+
1364+
Parameters
1365+
----------
1366+
bbox : 4-tuple or 3 tuple
1367+
* If bbox is a 4 tuple, it is the selected bounding box limits,
1368+
in *display* coordinates.
1369+
* If bbox is a 3 tuple, it is an (xp, yp, scl) triple, where
1370+
(xp, yp) is the center of zooming and scl the scale factor to
1371+
zoom by.
1372+
direction : str
1373+
The direction to apply the bounding box.
1374+
* `'in'` - The bounding box describes the view directly, i.e.,
1375+
it zooms in.
1376+
* `'out'` - The bounding box describes the size to make the
1377+
existing view, i.e., it zooms out.
1378+
mode : str or None
1379+
The selection mode, whether to apply the bounding box in only the
1380+
`'x'` direction, `'y'` direction or both (`None`).
1381+
twinx : bool
1382+
Whether this axis is twinned in the *x*-direction.
1383+
twiny : bool
1384+
Whether this axis is twinned in the *y*-direction.
1385+
"""
1386+
ax = self.ax
1387+
if len(bbox) == 3:
1388+
Xmin, Xmax = ax.get_xlim()
1389+
Ymin, Ymax = ax.get_ylim()
1390+
1391+
xp, yp, scl = bbox # Zooming code
1392+
1393+
if scl == 0: # Should not happen
1394+
scl = 1.
1395+
1396+
if scl > 1:
1397+
direction = 'in'
1398+
else:
1399+
direction = 'out'
1400+
scl = 1/scl
1401+
1402+
# get the limits of the axes
1403+
tranD2C = ax.transData.transform
1404+
xmin, ymin = tranD2C((Xmin, Ymin))
1405+
xmax, ymax = tranD2C((Xmax, Ymax))
1406+
1407+
# set the range
1408+
xwidth = xmax - xmin
1409+
ywidth = ymax - ymin
1410+
xcen = (xmax + xmin)*.5
1411+
ycen = (ymax + ymin)*.5
1412+
xzc = (xp*(scl - 1) + xcen)/scl
1413+
yzc = (yp*(scl - 1) + ycen)/scl
1414+
1415+
bbox = [xzc - xwidth/2./scl, yzc - ywidth/2./scl,
1416+
xzc + xwidth/2./scl, yzc + ywidth/2./scl]
1417+
elif len(bbox) != 4:
1418+
# should be len 3 or 4 but nothing else
1419+
_api.warn_external(
1420+
"Warning in _set_view_from_bbox: bounding box is not a tuple "
1421+
"of length 3 or 4. Ignoring the view change.")
1422+
return
1423+
1424+
# Original limits.
1425+
xmin0, xmax0 = ax.get_xbound()
1426+
ymin0, ymax0 = ax.get_ybound()
1427+
# The zoom box in screen coords.
1428+
startx, starty, stopx, stopy = bbox
1429+
1430+
# Convert to data coords.
1431+
(startx, starty), (stopx, stopy) = ax.transData.inverted().transform(
1432+
[(startx, starty), (stopx, stopy)])
1433+
# Clip to axes limits.
1434+
xmin, xmax = np.clip(sorted([startx, stopx]), xmin0, xmax0)
1435+
ymin, ymax = np.clip(sorted([starty, stopy]), ymin0, ymax0)
1436+
# Don't double-zoom twinned axes or if zooming only the other axis.
1437+
if twinx or mode == "y":
1438+
xmin, xmax = xmin0, xmax0
1439+
if twiny or mode == "x":
1440+
ymin, ymax = ymin0, ymax0
1441+
1442+
if direction == "in":
1443+
new_xbound = xmin, xmax
1444+
new_ybound = ymin, ymax
1445+
1446+
elif direction == "out":
1447+
x_trf = ax.xaxis.get_transform()
1448+
sxmin0, sxmax0, sxmin, sxmax = x_trf.transform(
1449+
[xmin0, xmax0, xmin, xmax]) # To screen space.
1450+
factor = (sxmax0 - sxmin0) / (sxmax - sxmin) # Unzoom factor.
1451+
# Move original bounds away by
1452+
# (factor) x (distance between unzoom box and axes bbox).
1453+
sxmin1 = sxmin0 - factor * (sxmin - sxmin0)
1454+
sxmax1 = sxmax0 + factor * (sxmax0 - sxmax)
1455+
# And back to data space.
1456+
new_xbound = x_trf.inverted().transform([sxmin1, sxmax1])
1457+
1458+
y_trf = ax.yaxis.get_transform()
1459+
symin0, symax0, symin, symax = y_trf.transform(
1460+
[ymin0, ymax0, ymin, ymax])
1461+
factor = (symax0 - symin0) / (symax - symin)
1462+
symin1 = symin0 - factor * (symin - symin0)
1463+
symax1 = symax0 + factor * (symax0 - symax)
1464+
new_ybound = y_trf.inverted().transform([symin1, symax1])
1465+
1466+
norm = self.mappable.norm
1467+
if self.orientation == 'horizontal' and not twinx and mode != "y":
1468+
norm.vmin, norm.vmax = new_xbound
1469+
if self.orientation == 'vertical' and not twiny and mode != "x":
1470+
norm.vmin, norm.vmax = new_ybound
1471+
self.update_normal(self.mappable)
1472+
1473+
def start_pan(self, x, y, button):
1474+
"""
1475+
Called when a pan operation has started.
1476+
1477+
Parameters
1478+
----------
1479+
x, y : float
1480+
The mouse coordinates in display coords.
1481+
button : `.MouseButton`
1482+
The pressed mouse button.
1483+
"""
1484+
self._pan_start = types.SimpleNamespace(
1485+
lim=self.ax.viewLim.frozen(),
1486+
trans=self.ax.transData.frozen(),
1487+
trans_inverse=self.ax.transData.inverted().frozen(),
1488+
bbox=self.ax.bbox.frozen(),
1489+
x=x,
1490+
y=y)
1491+
1492+
def end_pan(self):
1493+
"""
1494+
Called when a pan operation completes (when the mouse button is up.)
1495+
"""
1496+
del self._pan_start
1497+
1498+
def drag_pan(self, button, key, x, y):
1499+
"""
1500+
Called when the mouse moves during a pan operation.
1501+
1502+
Parameters
1503+
----------
1504+
button : `.MouseButton`
1505+
The pressed mouse button.
1506+
key : str or None
1507+
The pressed key, if any.
1508+
x, y : float
1509+
The mouse coordinates in display coords.
1510+
"""
1511+
def format_deltas(key, dx, dy):
1512+
if key == 'control':
1513+
if abs(dx) > abs(dy):
1514+
dy = dx
1515+
else:
1516+
dx = dy
1517+
elif key == 'x':
1518+
dy = 0
1519+
elif key == 'y':
1520+
dx = 0
1521+
elif key == 'shift':
1522+
if 2 * abs(dx) < abs(dy):
1523+
dx = 0
1524+
elif 2 * abs(dy) < abs(dx):
1525+
dy = 0
1526+
elif abs(dx) > abs(dy):
1527+
dy = dy / abs(dy) * abs(dx)
1528+
else:
1529+
dx = dx / abs(dx) * abs(dy)
1530+
return dx, dy
1531+
1532+
p = self._pan_start
1533+
dx = x - p.x
1534+
dy = y - p.y
1535+
if dx == dy == 0:
1536+
return
1537+
if button == 1:
1538+
dx, dy = format_deltas(key, dx, dy)
1539+
result = p.bbox.translated(-dx, -dy).transformed(p.trans_inverse)
1540+
elif button == 3:
1541+
try:
1542+
dx = -dx / self.ax.bbox.width
1543+
dy = -dy / self.ax.bbox.height
1544+
dx, dy = format_deltas(key, dx, dy)
1545+
if self.ax.get_aspect() != 'auto':
1546+
dx = dy = 0.5 * (dx + dy)
1547+
alpha = np.power(10.0, (dx, dy))
1548+
start = np.array([p.x, p.y])
1549+
oldpoints = p.lim.transformed(p.trans)
1550+
newpoints = start + alpha * (oldpoints - start)
1551+
result = (mtransforms.Bbox(newpoints)
1552+
.transformed(p.trans_inverse))
1553+
except OverflowError:
1554+
_api._warn_external('Overflow while panning')
1555+
return
1556+
else:
1557+
return
1558+
1559+
valid = np.isfinite(result.transformed(p.trans))
1560+
points = result.get_points().astype(object)
1561+
# Just ignore invalid limits (typically, underflow in log-scale).
1562+
points[~valid] = None
1563+
1564+
norm = self.mappable.norm
1565+
if self.orientation == 'horizontal':
1566+
norm.vmin, norm.vmax = points[:, 0]
1567+
if self.orientation == 'vertical':
1568+
norm.vmin, norm.vmax = points[:, 1]
1569+
self.update_normal(self.mappable)
1570+
13291571

13301572
def _normalize_location_orientation(location, orientation):
13311573
if location is None:

0 commit comments

Comments
 (0)