8000 Further progress on arbitrary transformations -- zooming and panning · matplotlib/matplotlib@16199c9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 16199c9

Browse files
committed
Further progress on arbitrary transformations -- zooming and panning
now works without any log-scale-specific hacks. (Though the underlying model is slightly wrong.) Added graphviz output support for debugging transformation trees. Masked array handling much more robust. svn path=/branches/transforms/; revision=3872
1 parent 36bce30 commit 16199c9

File tree

5 files changed

+237
-120
lines changed

5 files changed

+237
-120
lines changed

lib/matplotlib/axes.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -637,10 +637,14 @@ def _set_lim_and_transforms(self):
637637
# self.viewLim, self.bbox)
638638
self.preDataTransform = mtransforms.BboxTransform(
639639
self.viewLim, mtransforms.Bbox.unit())
640-
self.dataTransform = mtransforms.TestLogTransform()
641-
# self.dataTransform = mtransforms.Affine2D().scale(1.5)
640+
# self.dataTransform = mtransforms.TestPolarTransform()
641+
# self.dataTransform = mtransforms.blended_transform_factory(
642+
# mtransforms.TestLogTransform(),
643+
# mtransforms.Affine2D())
644+
self.dataTransform = mtransforms.Affine2D()
642645
self.transData = self.preDataTransform + self.dataTransform + mtransforms.BboxTransform(
643646
mtransforms.Bbox.unit(), self.bbox)
647+
self.transData.make_graphviz(open("trans.dot", "w"))
644648

645649

646650
def get_position(self, original=False):
@@ -1523,7 +1527,7 @@ def get_xscale(self):
15231527
'return the xaxis scale string: log or linear'
15241528
# MGDTODO
15251529
# return self.scaled[self.transData.get_funcx().get_type()]
1526-
return 'linear'
1530+
return 'log'
15271531

15281532
def set_xscale(self, value, basex = 10, subsx=None):
15291533
"""

lib/matplotlib/backend_bases.py

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,60 +1655,30 @@ def format_deltas(event,dx,dy):
16551655
#multiple button can get pressed during motion...
16561656
if self._button_pressed==1:
16571657
inverse = trans.inverted()
1658-
lastx, lasty = inverse.transform_point((lastx, lasty))
1659-
x, y = inverse.transform_point( (event.x, event.y) )
1660-
if a.get_xscale()=='log':
1661-
dx=1-lastx/x
1662-
else:
1663-
dx=x-lastx
1664-
if a.get_yscale()=='log':
1665-
dy=1-lasty/y
1666-
else:
1667-
dy=y-lasty
1668-
1669-
dx,dy=format_deltas(event,dx,dy)
1670-
1671-
if a.get_xscale()=='log':
1672-
xmin *= 1-dx
1673-
xmax *= 1-dx
1674-
else:
1675-
xmin -= dx
1676-
xmax -= dx
1677-
if a.get_yscale()=='log':
1678-
ymin *= 1-dy
1679-
ymax *= 1-dy
1680-
else:
1681-
ymin -= dy
1682-
ymax -= dy
1658+
dx, dy = event.x - lastx, event.y - lasty
1659+
dx, dy = format_deltas(event, dx, dy)
1660+
delta = npy.array([[dx, dy], [dx, dy]], npy.float_)
1661+
bbox = transforms.Bbox(a.bbox.get_points() - delta)
1662+
result = bbox.transformed(inverse)
16831663
elif self._button_pressed==3:
16841664
try:
1665+
inverse = trans.inverted()
16851666
dx=(lastx-event.x)/float(a.bbox.width)
16861667
dy=(lasty-event.y)/float(a.bbox.height)
1687-
dx,dy=format_deltas(event,dx,dy)
1688-
if a.get_aspect() != 'auto':
1689-
dx = 0.5*(dx + dy)
1690-
dy = dx
1691-
alphax = pow(10.0,dx)
1692-
alphay = pow(10.0,dy)#use logscaling, avoid singularities and smother scaling...
1693-
inverse = trans.inverted()
1694-
lastx, lasty = inverse.transform_point( (lastx, lasty) )
1695-
if a.get_xscale()=='log':
1696-
xmin = lastx*(xmin/lastx)**alphax
1697-
xmax = lastx*(xmax/lastx)**alphax
1698-
else:
1699-
xmin = lastx+alphax*(xmin-lastx)
1700-
xmax = lastx+alphax*(xmax-lastx)
1701-
if a.get_yscale()=='log':
1702-
ymin = lasty*(ymin/lasty)**alphay
1703-
ymax = lasty*(ymax/lasty)**alphay
1704-
else:
1705-
ymin = lasty+alphay*(ymin-lasty)
1706-
ymax = lasty+alphay*(ymax-lasty)
1668+
alphax = pow(10.0, dx)
1669+
alphay = pow(10.0, dy)
1670+
# MGDTODO: Make better use of numpy
1671+
lastx, lasty = inverse.transform_point((lastx, lasty))
1672+
xmin = (lastx + alphax * (xmin - lastx))
1673+
xmax = (lastx + alphax * (xmax - lastx))
1674+
ymin = (lasty + alphay * (ymin - lasty))
1675+
ymax = (lasty + alphay * (ymax - lasty))
1676+
result = transforms.Bbox.from_lbrt(xmin, ymin, xmax, ymax)
17071677
except OverflowError:
17081678
warnings.warn('Overflow while panning')
17091679
return
1710-
a.set_xlim(xmin, xmax)
1711-
a.set_ylim(ymin, ymax)
1680+
a.set_xlim(*result.intervalx)
1681+
a.set_ylim(*result.intervaly)
17121682

17131683
self.dynamic_update()
17141684

lib/matplotlib/lines.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,53 @@
2525
(TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN,
2626
CARETLEFT, CARETRIGHT, CARETUP, CARETDOWN) = range(8)
2727

28+
def unmasked_index_ranges(mask, compressed = True):
29+
'''
30+
Calculate the good data ranges in a masked 1-D npy.array, based on mask.
31+
32+
Returns Nx2 npy.array with each row the start and stop indices
33+
for slices of the compressed npy.array corresponding to each of N
34+
uninterrupted runs of unmasked values.
35+
If optional argument compressed is False, it returns the
36+
start and stop indices into the original npy.array, not the
37+
compressed npy.array.
38+
Returns None if there are no unmasked values.
39+
40+
Example:
41+
42+
y = ma.array(npy.arange(5), mask = [0,0,1,0,0])
43+
#ii = unmasked_index_ranges(y.mask())
44+
ii = unmasked_index_ranges(ma.getmask(y))
45+
# returns [[0,2,] [2,4,]]
46+
47+
y.compressed().filled()[ii[1,0]:ii[1,1]]
48+
# returns npy.array [3,4,]
49+
# (The 'filled()' method converts the masked npy.array to a numerix npy.array.)
50+
51+
#i0, i1 = unmasked_index_ranges(y.mask(), compressed=False)
52+
i0, i1 = unmasked_index_ranges(ma.getmask(y), compressed=False)
53+
# returns [[0,3,] [2,5,]]
54+
55+
y.filled()[ii[1,0]:ii[1,1]]
56+
# returns npy.array [3,4,]
57+
58+
'''
59+
m = npy.concatenate(((1,), mask, (1,)))
60+
indices = npy.arange(len(mask) + 1)
61+
mdif = m[1:] - m[:-1]
62+
i0 = npy.compress(mdif == -1, indices)
63+
i1 = npy.compress(mdif == 1, indices)
64+
assert len(i0) == len(i1)
65+
if len(i1) == 0:
66+
return None
67+
if not compressed:
68+
return npy.concatenate((i0[:, npy.newaxis], i1[:, npy.newaxis]), axis=1)
69+
seglengths = i1 - i0
70+
breakpoints = npy.cumsum(seglengths)
71+
ic0 = npy.concatenate(((0,), breakpoints[:-1]))
72+
ic1 = breakpoints
73+
return npy.concatenate((ic0[:, npy.newaxis], ic1[:, npy.newaxis]), axis=1)
74+
2875
def segment_hits(cx,cy,x,y,radius):
2976
"""Determine if any line segments are within radius of a point. Returns
3077
the list of line segments that are within that radius.
@@ -302,7 +349,7 @@ def set_picker(self,p):
302349
self._picker = p
303350

304351
def get_window_extent(self, renderer):
305-
xy = self.get_transform()(self._xy)
352+
xy = self.get_transform().transform(self._xy)
306353

307354
x = xy[:, 0]
308355
y = xy[:, 1]
@@ -343,9 +390,6 @@ def set_data(self, *args):
343390
self._yorig = y
344391
self.recache()
345392

346-
# MGDTODO: Masked data arrays are broken
347-
_masked_array_to_path_code_mapping = npy.array(
348-
[Path.LINETO, Path.MOVETO, Path.MOVETO], Path.code_type)
349393
def recache(self):
350394
#if self.axes is None: print 'recache no axes'
351395
#else: print 'recache units', self.axes.xaxis.units, self.axes.yaxis.units
@@ -363,24 +407,15 @@ def recache(self):
363407
if len(x) != len(y):
364408
raise RuntimeError('xdata and ydata must be the same length')
365409

366-
self._xy = npy.vstack((npy.asarray(x, npy.float_),
367-
npy.asarray(y, npy.float_))).transpose()
410+
x = x.reshape((len(x), 1))
411+
y = y.reshape((len(y), 1))
412+
413+
self._xy = ma.concatenate((x, y), 1)
368414
self._x = self._xy[:, 0] # just a view
369415
self._y = self._xy[:, 1] # just a view
370416
self._logcache = None
371-
372-
mx = ma.getmask(x)
373-
my = ma.getmask(y)
374-
mask = ma.mask_or(mx, my)
375-
codes = None
376-
if mask is not ma.nomask:
377-
m = npy.concatenate(((1,), mask, (1,)))
378-
mdif = m[1:] - m[:-1]
379-
mdif = npy.maximum((mdif[:-1] * -2), mask)
380-
codes = npy.take(
381-
self._masked_array_to_path_code_mapping,
382-
mdif)
383-
self._path = Path(self._xy, codes, closed=False)
417+
# Masked arrays are now handled by the Path class itself
418+
self._path = Path(self._xy, closed=False)
384419
# MGDTODO: If _draw_steps is removed, remove the following line also
385420
self._step_path = None
386421

lib/matplotlib/path.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as npy
2+
from numpy import ma as ma
23

34
class Path(object):
45
# Path codes
@@ -21,10 +22,8 @@ class Path(object):
2122
code_type = npy.uint8
2223

2324
def __init__(self, vertices, codes=None, closed=True):
24-
vertices = npy.asarray(vertices, npy.float_)
25-
assert vertices.ndim == 2
26-
assert vertices.shape[1] == 2
27-
25+
vertices = ma.asarray(vertices, npy.float_)
26+
2827
if codes is None:
2928
if closed:
3029
codes = self.LINETO * npy.ones(
@@ -41,10 +40,27 @@ def __init__(self, vertices, codes=None, closed=True):
4140
assert codes.ndim == 1
4241
assert len(codes) == len(vertices)
4342

43+
# The path being passed in may have masked values. However,
44+
# the backends are not expected to deal with masked arrays, so
45+
# we must remove them from the array (using compressed), and
46+
# add MOVETO commands to the codes array accordingly.
47+
mask = ma.getmask(vertices)
48+
if mask is not ma.nomask:
49+
mask1d = ma.mask_or(mask[:, 0], mask[:, 1])
50+
vertices = ma.compress(npy.invert(mask1d), vertices, 0)
51+
codes = npy.where(npy.concatenate((mask1d[-1:], mask1d[:-1])),
52+
self.MOVETO, codes)
53+
codes = ma.masked_array(codes, mask=mask1d).compressed()
54+
codes = npy.asarray(codes, self.code_type)
55+
56+
vertices = npy.asarray(vertices, npy.float_)
57+
58+
assert vertices.ndim == 2
59+
assert vertices.shape[1] == 2
60+
assert codes.ndim == 1
61+
4462
self._codes = codes
4563
self._vertices = vertices
46-
47-
assert self._codes.ndim == 1
4864

4965
def __repr__(self):
5066
return "Path(%s, %s)" % (self.vertices, self.codes)
@@ -91,10 +107,11 @@ def unit_rectangle(cls):
91107
def unit_regular_polygon(cls, numVertices):
92108
path = cls._unit_regular_polygons.get(numVertices)
93109
if path is None:
94-
theta = 2*npy.pi/numVertices * npy.arange(numVertices)
95-
# This is to make sure the polygon always "points-up"
110+
theta = 2*npy.pi/numVertices * npy.arange(numVertices).reshape((numVertices, 1))
111+
# This initial rotation is to make sure the polygon always
112+
# "points-up"
96113
theta += npy.pi / 2.0
97-
verts = npy.vstack((npy.cos(theta), npy.sin(theta))).transpose()
114+
verts = npy.concatenate((npy.cos(theta), npy.sin(theta)))
98115
path = Path(verts)
99116
cls._unit_regular_polygons[numVertices] = path
100117
return path

0 commit comments

Comments
 (0)
0