Skip to content

Commit a93b887

Browse files
authored
Merge pull request #8224 from MaartenBaert/faster-legend-loc-best-squashed
Implement Path.intersects_bbox in C++ to speed up legend positioning.
2 parents ca2ac85 + 3bcfeca commit a93b887

File tree

5 files changed

+1810
-7
lines changed

5 files changed

+1810
-7
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Path.intersects_bbox always treats the bounding box as filled
2+
`````````````````````````````````````````````````````````````
3+
4+
Previously, when ``Path.intersects_bbox`` was called with ``filled`` set to
5+
``False``, it would treat both the path and the bounding box as unfilled. This
6+
behavior was not well documented and it is usually not the desired behavior,
7+
since bounding boxes are used to represent more complex shapes located inside
8+
the bounding box. This behavior has now been changed: when ``filled`` is
9+
``False``, the path will be treated as unfilled, but the bounding box is still
10+
treated as filled. The old behavior was arguably an implementation bug.
11+
12+
When ``Path.intersects_bbox`` is called with ``filled`` set to ``True``
13+
(the default value), there is no change in behavior. For those rare cases where
14+
``Path.intersects_bbox`` was called with ``filled`` set to ``False`` and where
15+
the old behavior is actually desired, the suggested workaround is to call
16+
``Path.intersects_path`` with a rectangle as the path::
17+
18+
from matplotlib.path import Path
19+
from matplotlib.transforms import Bbox, BboxTransformTo
20+
rect = Path.unit_rectangle().transformed(BboxTransformTo(bbox))
21+
result = path.intersects_path(rect, filled=False)

lib/matplotlib/path.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -558,14 +558,13 @@ def intersects_bbox(self, bbox, filled=True):
558558
:class:`~matplotlib.transforms.Bbox`.
559559
560560
*filled*, when True, treats the path as if it was filled.
561-
That is, if one path completely encloses the other,
562-
:meth:`intersects_path` will return True.
561+
That is, if the path completely encloses the bounding box,
562+
:meth:`intersects_bbox` will return True.
563+
564+
The bounding box is always considered filled.
563565
"""
564-
from .transforms import BboxTransformTo
565-
rectangle = self.unit_rectangle().transformed(
566-
BboxTransformTo(bbox))
567-
result = self.intersects_path(rectangle, filled)
568-
return result
566+
return _path.path_intersects_rectangle(self,
567+
bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled)
569568

570569
def interpolated(self, steps):
571570
"""

src/_path.h

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,66 @@ bool path_intersects_path(PathIterator1 &p1, PathIterator2 &p2)
872872
return false;
873873
}
874874

875+
// returns whether the segment from (x1,y1) to (x2,y2)
876+
// intersects the rectangle centered at (cx,cy) with size (w,h)
877+
// see doc/segment_intersects_rectangle.svg for a more detailed explanation
878+
inline bool segment_intersects_rectangle(double x1, double y1,
879+
double x2, double y2,
880+
double cx, double cy,
881+
double w, double h)
882+
{
883+
return fabs(x1 + x2 - 2.0 * cx) < fabs(x1 - x2) + w &&
884+
fabs(y1 + y2 - 2.0 * cy) < fabs(y1 - y2) + h &&
885+
2.0 * fabs((x1 - cx) * (y1 - y2) - (y1 - cy) * (x1 - x2)) <
886+
w * fabs(y1 - y2) + h * fabs(x1 - x2);
887+
}
888+
889+
template <class PathIterator>
890+
bool path_intersects_rectangle(PathIterator &path,
891+
double rect_x1, double rect_y1,
892+
double rect_x2, double rect_y2,
893+
bool filled)
894+
{
895+
typedef PathNanRemover<py::PathIterator> no_nans_t;
896+
typedef agg::conv_curve<no_nans_t> curve_t;
897+
898+
if (path.total_vertices() == 0) {
899+
return false;
900+
}
901+
902+
no_nans_t no_nans(path, true, path.has_curves());
903+
curve_t curve(no_nans);
904+
905+
double cx = (rect_x1 + rect_x2) * 0.5, cy = (rect_y1 + rect_y2) * 0.5;
906+
double w = fabs(rect_x1 - rect_x2), h = fabs(rect_y1 - rect_y2);
907+
double xmin = std::min(rect_x1, rect_x2), xmax = std::max(rect_x1, rect_x2);
908+
double ymin = std::min(rect_x1, rect_x2), ymax = std::max(rect_x1, rect_x2);
909+
910+
double x1, y1, x2, y2;
911+
912+
curve.vertex(&x1, &y1);
913+
if (2.0 * fabs(x1 - cx) <= w && 2.0 * fabs(y1 - cy) <= h) {
914+
return true;
915+
}
916+
917+
while (curve.vertex(&x2, &y2) != agg::path_cmd_stop) {
918+
if (segment_intersects_rectangle(x1, y1, x2, y2, cx, cy, w, h)) {
919+
return true;
920+
}
921+
x1 = x2;
922+
y1 = y2;
923+
}
924+
925+
if (filled) {
926+
agg::trans_affine trans;
927+
if (point_in_path(cx, cy, 0.0, path, trans)) {
928+
return true;
929+
}
930+
}
931+
932+
return false;
933+
}
934+
875935
template <class PathIterator>
876936
void convert_path_to_polygons(PathIterator &path,
877937
agg::trans_affine &trans,

src/_path_wrapper.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,39 @@ static PyObject *Py_path_intersects_path(PyObject *self, PyObject *args, PyObjec
521521
}
522522
}
523523

524+
const char *Py_path_intersects_rectangle__doc__ = "path_intersects_rectangle(path, rect_x1, rect_y1, rect_x2, rect_y2, filled=False)";
525+
526+
static PyObject *Py_path_intersects_rectangle(PyObject *self, PyObject *args, PyObject *kwds)
527+
{
528+
py::PathIterator path;
529+
double rect_x1, rect_y1, rect_x2, rect_y2;
530+
int filled = 0;
531+
const char *names[] = { "path", "rect_x1", "rect_y1", "rect_x2", "rect_y2", "filled", NULL };
532+
bool result;
533+
534+
if (!PyArg_ParseTupleAndKeywords(args,
535+
kwds,
536+
"O&dddd|i:path_intersects_rectangle",
537+
(char **)names,
538+
&convert_path,
539+
&path,
540+
&rect_x1,
541+
&rect_y1,
542+
&rect_x2,
543+
&rect_y2,
544+
&filled)) {
545+
return NULL;
546+
}
547+
548+
CALL_CPP("path_intersects_rectangle", (result = path_intersects_rectangle(path, rect_x1, rect_y1, rect_x2, rect_y2, filled)));
549+
550+
if (result) {
551+
Py_RETURN_TRUE;
552+
} else {
553+
Py_RETURN_FALSE;
554+
}
555+
}
556+
524557
const char *Py_convert_path_to_polygons__doc__ =
525558
"convert_path_to_polygons(path, trans, width=0, height=0)";
526559

@@ -819,6 +852,7 @@ extern "C" {
819852
{"affine_transform", (PyCFunction)Py_affine_transform, METH_VARARGS, Py_affine_transform__doc__},
820853
{"count_bboxes_overlapping_bbox", (PyCFunction)Py_count_bboxes_overlapping_bbox, METH_VARARGS, Py_count_bboxes_overlapping_bbox__doc__},
821854
{"path_intersects_path", (PyCFunction)Py_path_intersects_path, METH_VARARGS|METH_KEYWORDS, Py_path_intersects_path__doc__},
855+
{"path_intersects_rectangle", (PyCFunction)Py_path_intersects_rectangle, METH_VARARGS|METH_KEYWORDS, Py_path_intersects_rectangle__doc__},
822856
{"convert_path_to_polygons", (PyCFunction)Py_convert_path_to_polygons, METH_VARARGS|METH_KEYWORDS, Py_convert_path_to_polygons__doc__},
823857
{"cleanup_path", (PyCFunction)Py_cleanup_path, METH_VARARGS, Py_cleanup_path__doc__},
824858
{"convert_to_string", (PyCFunction)Py_convert_to_string, METH_VARARGS, Py_convert_to_string__doc__},

0 commit comments

Comments
 (0)