-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Fix PDF path collection culling for hexbin offsets
#32000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2011,19 +2011,20 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, | |
| name = self.file.pathCollectionObject( | ||
| gc, path, transform, padding, filled, stroked) | ||
| path_codes.append(name) | ||
| # Compute the extent of each marker path to enable per-marker | ||
| # bounds checking. This allows us to skip markers that are | ||
| # completely outside the visible canvas while preserving markers | ||
| # that are partially visible. | ||
| if len(path.vertices): | ||
| bbox = path.get_extents(transform) | ||
| # Store half-width and half-height for efficient bounds checking | ||
| path_extents.append((bbox.width / 2, bbox.height / 2)) | ||
| else: | ||
| path_extents.append((0, 0)) | ||
| # Compute each transformed path's exact bounds for per-marker | ||
| # canvas checks. Offsets are not necessarily full canvas-space | ||
| # centers, e.g. for collections using AffineDeltaTransform, so | ||
| # cull based on the final path bounds translated by the offset. | ||
| path_extents.append(path.get_extents(transform).frozen()) | ||
|
|
||
| # Create a mapping from path_id to extent for efficient lookup | ||
| path_extent_map = dict(zip(path_codes, path_extents)) | ||
| # for cases with multiple paths | ||
| if len(path_codes) == 1: | ||
| single_path_extent = path_extents[0] | ||
| path_extent_map = None | ||
| else: | ||
| single_path_extent = None | ||
| path_extent_map = dict(zip(path_codes, path_extents)) | ||
|
|
||
| canvas_width = self.file.width * 72 | ||
| canvas_height = self.file.height * 72 | ||
|
|
@@ -2036,26 +2037,12 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, | |
| facecolors, edgecolors, linewidths, linestyles, | ||
| antialiaseds, urls, offset_position, hatchcolors=hatchcolors): | ||
|
|
||
| # Optimization: Fast path for markers with centers inside canvas. | ||
| # This avoids the dictionary lookup for the common case where | ||
| # markers are visible, improving performance for large scatter plots. | ||
| if 0 <= xo <= canvas_width and 0 <= yo <= canvas_height: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to look at Alternatively, would it make sense to apply the offset_trans when doing the check?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I understood from It is anyway probably wrong to refer only to the offset values without seeing the reference |
||
| # Marker center is inside canvas - definitely render it | ||
| self.check_gc(gc0, rgbFace) | ||
| dx, dy = xo - lastx, yo - lasty | ||
| output(1, 0, 0, 1, dx, dy, Op.concat_matrix, path_id, | ||
| Op.use_xobject) | ||
| lastx, lasty = xo, yo | ||
| continue | ||
|
|
||
| # Marker center is outside canvas - check if partially visible. | ||
| # Skip markers completely outside visible canvas bounds to reduce | ||
| # PDF file size. Use per-marker extents to handle large markers | ||
| # correctly: only skip if the marker's bounding box doesn't | ||
| # intersect the canvas at all. | ||
| extent_x, extent_y = path_extent_map[path_id] | ||
| if not (-extent_x <= xo <= canvas_width + extent_x | ||
| and -extent_y <= yo <= canvas_height + extent_y): | ||
| # Skip markers completely outside the canvas to reduce PDF size. | ||
| # Use the translated path bounds, not the offset alone: the offset | ||
| # need not be the marker center in canvas coordinates. | ||
| bbox = path_extent_map[path_id] if path_extent_map else single_path_extent | ||
| if (bbox.x1 + xo < 0 or bbox.x0 + xo > canvas_width | ||
| or bbox.y1 + yo < 0 or bbox.y0 + yo > canvas_height): | ||
| continue | ||
|
|
||
| self.check_gc(gc0, rgbFace) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have any benchmarks on what we are giving up performance-wise here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree on your concern and would like to suggest an alternative approach as follows.
If I understand correctly, the "optimization" here essentially meant to avoid the look-up of the
path_extent_mapdictionary.We can actually avoid this look-up in a different viewpoint.
For most use cases of
Collectionviascatterandhexbin, thepathslist has actually only a single element.In such a case, we do not have to make a dictionary and do not need to look it up.
In the newly added commit, I implement the idea above. This should retain the original "optimization" in the sense of avoiding the dictionary look-up.