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

Skip to content

Commit 4dd3de1

Browse files
committed
Make hexbin much faster by creating a PathCollection that is a single polygon and a number of offsets. This allows the Agg backend to be dealing with far less data, and it allows the vector backends to write the polygon once and "use" it multiple times.
1 parent 85cfca2 commit 4dd3de1

10 files changed

Lines changed: 157 additions & 46 deletions

File tree

examples/pylab_examples/hexbin_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import matplotlib.cm as cm
1010
import matplotlib.pyplot as plt
1111

12+
np.random.seed(0)
1213
n = 100000
1314
x = np.random.standard_normal(n)
1415
y = 2.0 + 3.0 * x + 4.0 * np.random.standard_normal(n)
@@ -33,4 +34,3 @@
3334
cb.set_label('log10(N)')
3435

3536
plt.show()
36-

lib/matplotlib/axes.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6175,43 +6175,47 @@ def hexbin(self, x, y, C = None, gridsize = 100, bins = None,
61756175
lattice1.astype(float).ravel(), lattice2.astype(float).ravel()))
61766176
good_idxs = ~np.isnan(accum)
61776177

6178-
px = xmin + sx * np.array([ 0.5, 0.5, 0.0, -0.5, -0.5, 0.0])
6179-
py = ymin + sy * np.array([-0.5, 0.5, 1.0, 0.5, -0.5, -1.0]) / 3.0
6180-
6181-
polygons = np.zeros((6, n, 2), float)
6182-
polygons[:,:nx1*ny1,0] = np.repeat(np.arange(nx1), ny1)
6183-
polygons[:,:nx1*ny1,1] = np.tile(np.arange(ny1), nx1)
6184-
polygons[:,nx1*ny1:,0] = np.repeat(np.arange(nx2) + 0.5, ny2)
6185-
polygons[:,nx1*ny1:,1] = np.tile(np.arange(ny2), nx2) + 0.5
6186-
6178+
px = sx * np.array([ 0.5, 0.5, 0.0, -0.5, -0.5, 0.0])
6179+
py = sy * np.array([-0.5, 0.5, 1.0, 0.5, -0.5, -1.0]) / 3.0
6180+
6181+
offsets = np.zeros((n, 2), float)
6182+
offsets[:nx1*ny1,0] = np.repeat(np.arange(nx1), ny1)
6183+
offsets[:nx1*ny1,1] = np.tile(np.arange(ny1), nx1)
6184+
offsets[nx1*ny1:,0] = np.repeat(np.arange(nx2) + 0.5, ny2)
6185+
offsets[nx1*ny1:,1] = np.tile(np.arange(ny2), nx2) + 0.5
6186+
offsets[:,0] *= sx
6187+
offsets[:,1] *= sy
6188+
offsets[:,0] += xmin
6189+
offsets[:,1] += ymin
61876190
# remove accumulation bins with no data
6188-
polygons = polygons[:,good_idxs,:]
6191+
offsets = offsets[good_idxs,:]
61896192
accum = accum[good_idxs]
61906193

6191-
polygons = np.transpose(polygons, axes=[1,0,2])
6192-
polygons[:,:,0] *= sx
6193-
polygons[:,:,1] *= sy
6194-
polygons[:,:,0] += px
6195-
polygons[:,:,1] += py
6196-
61976194
if xscale=='log':
6198-
polygons[:,:,0] = 10**(polygons[:,:,0])
6195+
offsets[:,0] = 10**(offsets[:,0])
61996196
xmin = 10**xmin
62006197
xmax = 10**xmax
62016198
self.set_xscale('log')
62026199
if yscale=='log':
6203-
polygons[:,:,1] = 10**(polygons[:,:,1])
6200+
offsets[:,1] = 10**(offsets[:,1])
62046201
ymin = 10**ymin
62056202
ymax = 10**ymax
62066203
self.set_yscale('log')
62076204

6205+
polygons = np.zeros((6, 2), float)
6206+
polygons[:,0] = px
6207+
polygons[:,1] = py
6208+
62086209
if edgecolors=='none':
62096210
edgecolors = 'face'
6211+
62106212
collection = mcoll.PolyCollection(
6211-
polygons,
6213+
[polygons],
62126214
edgecolors = edgecolors,
62136215
linewidths = linewidths,
6214-
transOffset = self.transData,
6216+
offsets = offsets,
6217+
transOffset = mtransforms.IdentityTransform(),
6218+
offset_position = "data"
62156219
)
62166220

62176221
if isinstance(norm, mcolors.LogNorm):

lib/matplotlib/backend_bases.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,16 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None)
186186

187187
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
188188
offsets, offsetTrans, facecolors, edgecolors,
189-
linewidths, linestyles, antialiaseds, urls):
189+
linewidths, linestyles, antialiaseds, urls,
190+
offset_position):
190191
"""
191192
Draws a collection of paths selecting drawing properties from
192193
the lists *facecolors*, *edgecolors*, *linewidths*,
193194
*linestyles* and *antialiaseds*. *offsets* is a list of
194195
offsets to apply to each of the paths. The offsets in
195196
*offsets* are first transformed by *offsetTrans* before being
196-
applied.
197+
applied. *offset_position* may be either "screen" or "data"
198+
depending on the space that the offsets are in.
197199
198200
This provides a fallback implementation of
199201
:meth:`draw_path_collection` that makes multiple calls to
@@ -213,8 +215,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
213215
path_ids.append((path, transform))
214216

215217
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
216-
gc, path_ids, offsets, offsetTrans, facecolors, edgecolors,
217-
linewidths, linestyles, antialiaseds, urls):
218+
gc, master_transform, all_transforms, path_ids, offsets,
219+
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
220+
antialiaseds, urls, offset_position):
218221
path, transform = path_id
219222
transform = transforms.Affine2D(transform.get_matrix()).translate(xo, yo)
220223
self.draw_path(gc0, path, transform, rgbFace)
@@ -240,7 +243,7 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
240243

241244
return self.draw_path_collection(
242245
gc, master_transform, paths, [], offsets, offsetTrans, facecolors,
243-
edgecolors, linewidths, [], [antialiased], [None])
246+
edgecolors, linewidths, [], [antialiased], [None], 'screen')
244247

245248
def draw_gouraud_triangle(self, gc, points, colors, transform):
246249
"""
@@ -302,9 +305,10 @@ def _iter_collection_raw_paths(self, master_transform, paths,
302305
transform = all_transforms[i % Ntransforms]
303306
yield path, transform + master_transform
304307

305-
def _iter_collection(self, gc, path_ids, offsets, offsetTrans, facecolors,
306-
edgecolors, linewidths, linestyles, antialiaseds,
307-
urls):
308+
def _iter_collection(self, gc, master_transform, all_transforms,
309+
path_ids, offsets, offsetTrans, facecolors,
310+
edgecolors, linewidths, linestyles,
311+
antialiaseds, urls, offset_position):
308312
"""
309313
This is a helper method (along with
310314
:meth:`_iter_collection_raw_paths`) to make it easier to write
@@ -330,6 +334,7 @@ def _iter_collection(self, gc, path_ids, offsets, offsetTrans, facecolors,
330334
*path_ids*; *gc* is a graphics context and *rgbFace* is a color to
331335
use for filling the path.
332336
"""
337+
Ntransforms = len(all_transforms)
333338
Npaths = len(path_ids)
334339
Noffsets = len(offsets)
335340
N = max(Npaths, Noffsets)
@@ -359,6 +364,15 @@ def _iter_collection(self, gc, path_ids, offsets, offsetTrans, facecolors,
359364
path_id = path_ids[i % Npaths]
360365
if Noffsets:
361366
xo, yo = toffsets[i % Noffsets]
367+
if offset_position == 'data':
368+
if Ntransforms:
369+
transform = all_transforms[i % Ntransforms] + master_transform
370+
else:
371+
transform = master_transform
372+
xo, yo = transform.transform_point((xo, yo))
373+
xp, yp = transform.transform_point((0, 0))
374+
xo = -(xp - xo)
375+
yo = -(yp - yo)
362376
if Nfacecolors:
363377
rgbFace = facecolors[i % Nfacecolors]
364378
if Nedgecolors:

lib/matplotlib/backends/backend_macosx.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None)
6060

6161
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
6262
offsets, offsetTrans, facecolors, edgecolors,
63-
linewidths, linestyles, antialiaseds, urls):
63+
linewidths, linestyles, antialiaseds, urls,
64+
offset_position):
6465
cliprect = gc.get_clip_rectangle()
6566
clippath, clippath_transform = gc.get_clip_path()
6667
if all_transforms:

lib/matplotlib/backends/backend_pdf.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,8 @@ def __init__(self, filename):
452452
self.markers = {}
453453
self.multi_byte_charprocs = {}
454454

455+
self.paths = []
456+
455457
# The PDF spec recommends to include every procset
456458
procsets = [ Name(x)
457459
for x in "PDF Text ImageB ImageC ImageI".split() ]
@@ -505,9 +507,12 @@ def close(self):
505507
xobjects[tup[0]] = tup[1]
506508
for name, value in self.multi_byte_charprocs.iteritems():
507509
xobjects[name] = value
510+
for name, path, trans, ob, join, cap, padding in self.paths:
511+
xobjects[name] = ob
508512
self.writeObject(self.XObjectObject, xobjects)
509513
self.writeImages()
510514
self.writeMarkers()
515+
self.writePathCollectionTemplates()
511516
self.writeObject(self.pagesObject,
512517
{ 'Type': Name('Pages'),
513518
'Kids': self.pageList,
@@ -1259,6 +1264,28 @@ def writeMarkers(self):
12591264
self.output(Op.paint_path(False, fillp, strokep))
12601265
self.endStream()
12611266

1267+
def pathCollectionObject(self, gc, path, trans, padding):
1268+
name = Name('P%d' % len(self.paths))
1269+
ob = self.reserveObject('path %d' % len(self.paths))
1270+
self.paths.append(
1271+
(name, path, trans, ob, gc.get_joinstyle(), gc.get_capstyle(), padding))
1272+
return name
1273+
1274+
def writePathCollectionTemplates(self):
1275+
for (name, path, trans, ob, joinstyle, capstyle, padding) in self.paths:
1276+
pathops = self.pathOperations(path, trans, simplify=False)
1277+
bbox = path.get_extents(trans)
1278+
bbox = bbox.padded(padding)
1279+
self.beginStream(
1280+
ob.id, None,
1281+
{'Type': Name('XObject'), 'Subtype': Name('Form'),
1282+
'BBox': list(bbox.extents)})
1283+
self.output(GraphicsContextPdf.joinstyles[joinstyle], Op.setlinejoin)
1284+
self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap)
1285+
self.output(*pathops)
1286+
self.output(Op.paint_path(False, True, True))
1287+
self.endStream()
1288+
12621289
@staticmethod
12631290
def pathOperations(path, transform, clip=None, simplify=None):
12641291
cmds = []
@@ -1466,6 +1493,32 @@ def draw_path(self, gc, path, transform, rgbFace=None):
14661493
rgbFace is None and gc.get_hatch_path() is None)
14671494
self.file.output(self.gc.paint())
14681495

1496+
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
1497+
offsets, offsetTrans, facecolors, edgecolors,
1498+
linewidths, linestyles, antialiaseds, urls,
1499+
offset_position):
1500+
1501+
padding = np.max(linewidths)
1502+
path_codes = []
1503+
for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
1504+
master_transform, paths, all_transforms)):
1505+
name = self.file.pathCollectionObject(gc, path, transform, padding)
1506+
path_codes.append(name)
1507+
1508+
output = self.file.output
1509+
output(Op.gsave)
1510+
lastx, lasty = 0, 0
1511+
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
1512+
gc, master_transform, all_transforms, path_codes, offsets,
1513+
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
1514+
antialiaseds, urls, offset_position):
1515+
1516+
self.check_gc(gc0, rgbFace)
1517+
dx, dy = xo - lastx, yo - lasty
1518+
output(1, 0, 0, 1, dx, dy, Op.concat_matrix, path_id, Op.use_xobject)
1519+
lastx, lasty = xo, yo
1520+
output(Op.grestore)
1521+
14691522
def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
14701523
# For simple paths or small numbers of markers, don't bother
14711524
# making an XObject

lib/matplotlib/backends/backend_ps.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,8 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None)
625625

626626
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
627627
offsets, offsetTrans, facecolors, edgecolors,
628-
linewidths, linestyles, antialiaseds, urls):
628+
linewidths, linestyles, antialiaseds, urls,
629+
offset_position):
629630
write = self._pswriter.write
630631

631632
path_codes = []
@@ -640,8 +641,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
640641
path_codes.append(name)
641642

642643
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
643-
gc, path_codes, offsets, offsetTrans, facecolors, edgecolors,
644-
linewidths, linestyles, antialiaseds, urls):
644+
gc, master_transform, all_transforms, path_codes, offsets,
645+
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
646+
antialiaseds, urls, offset_position):
645647
ps = "%g %g %s" % (xo, yo, path_id)
646648
self._draw_ps(ps, gc0, rgbFace)
647649

lib/matplotlib/backends/backend_svg.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,8 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None)
577577

578578
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
579579
offsets, offsetTrans, facecolors, edgecolors,
580-
linewidths, linestyles, antialiaseds, urls):
580+
linewidths, linestyles, antialiaseds, urls,
581+
offset_position):
581582
writer = self.writer
582583
path_codes = []
583584
writer.start(u'defs')
@@ -592,8 +593,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
592593
writer.end(u'defs')
593594

594595
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
595-
gc, path_codes, offsets, offsetTrans, facecolors, edgecolors,
596-
linewidths, linestyles, antialiaseds, urls):
596+
gc, master_transform, all_transforms, path_codes, offsets,
597+
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
598+
antialiaseds, urls, offset_position):
597599
clipid = self._get_clip(gc0)
598600
url = gc0.get_url()
599601
if url is not None:

lib/matplotlib/collections.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,19 @@ class Collection(artist.Artist, cm.ScalarMappable):
4242
* *antialiaseds*: None
4343
* *offsets*: None
4444
* *transOffset*: transforms.IdentityTransform()
45+
* *offset_position*: 'screen' (default) or 'data'
4546
* *norm*: None (optional for
4647
:class:`matplotlib.cm.ScalarMappable`)
4748
* *cmap*: None (optional for
4849
:class:`matplotlib.cm.ScalarMappable`)
4950
* *hatch*: None
5051
5152
*offsets* and *transOffset* are used to translate the patch after
52-
rendering (default no offsets).
53+
rendering (default no offsets). If offset_position is 'screen'
54+
(default) the offset is applied after the master transform has
55+
been applied, that is, the offsets are in screen coordinates. If
56+
offset_position is 'data', the offset is applied before the master
57+
transform, i.e., the offsets are in data coordinates.
5358
5459
If any of *edgecolors*, *facecolors*, *linewidths*, *antialiaseds*
5560
are None, they default to their :data:`matplotlib.rcParams` patch
@@ -80,6 +85,7 @@ def __init__(self,
8085
pickradius = 5.0,
8186
hatch=None,
8287
urls = None,
88+
offset_position='screen',
8389
**kwargs
8490
):
8591
"""
@@ -98,7 +104,7 @@ def __init__(self,
98104
self.set_pickradius(pickradius)
99105
self.set_urls(urls)
100106
self.set_hatch(hatch)
101-
107+
self.set_offset_position(offset_position)
102108

103109
self._uniform_offsets = None
104110
self._offsets = np.array([], np.float_)
@@ -242,7 +248,8 @@ def draw(self, renderer):
242248
renderer.draw_path_collection(
243249
gc, transform.frozen(), paths, self.get_transforms(),
244250
offsets, transOffset, self.get_facecolor(), self.get_edgecolor(),
245-
self._linewidths, self._linestyles, self._antialiaseds, self._urls)
251+
self._linewidths, self._linestyles, self._antialiaseds, self._urls,
252+
self._offset_position)
246253

247254
gc.restore()
248255
renderer.close_group(self.__class__.__name__)
@@ -359,6 +366,24 @@ def get_offsets(self):
359366
else:
360367
return self._uniform_offsets
361368

369+
def set_offset_position(self, offset_position):
370+
"""
371+
The how offsets are applied. If *offset_position* is 'screen'
372+
(default) the offset is applied after the master transform has
373+
been applied, that is, the offsets are in screen coordinates.
374+
If offset_position is 'data', the offset is applied before the
375+
master transform, i.e., the offsets are in data coordinates.
376+
"""
377+
if offset_position not in ('screen', 'data'):
378+
raise ValueError("offset_position must be 'screen' or 'data'")
379+
self._offset_position = offset_position
380+
381+
def get_offset_position(self):
382+
"""
383+
Returns the offset position of the collection.
384+
"""
385+
return self._offset_position
386+
362387
def set_linewidth(self, lw):
363388
"""
364389
Set the linewidth(s) for the collection. *lw* can be a scalar

0 commit comments

Comments
 (0)