You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
_draw_blocks creates one matplotlib.patches.Rectangle per block and adds it with ax.add_artist. matplotlib then transforms and draws each one individually, so both building and drawing scale linearly with an expensive per-block constant.
For a normal chart of tens or hundreds of blocks this is invisible and not worth changing. It matters for the thousand-plus-block charts that #2 was about, and it is the reason MAX_BLOCKS exists as a backstop rather than as a considered limit.
Measured
Three categories, one chart, timed as build plus canvas.draw(), best of five:
blocks
current
one PatchCollection
one PolyCollection from a vertex array
1,000
0.070s
0.035s
0.024s
10,000
0.701s
0.290s
0.030s
50,000
3.711s
1.496s
0.129s
Split into build and draw at 50,000 blocks:
build
draw
current
1.746s
1.965s
PatchCollection
1.429s
0.067s
PolyCollection
0.069s
0.060s
A PatchCollection fixes drawing but not building, because constructing 50,000 Rectangle objects in Python is what dominates once drawing is cheap. Building the vertex array directly, with no per-block Python object at all, is what removes the rest. 29x faster overall at 50,000 blocks.
Where it starts to pay
blocks
current
PolyCollection
speedup
50
0.0051s
0.0078s
0.7x
100
0.0078s
0.0080s
1.0x
500
0.0300s
0.0095s
3.2x
1,000
0.0589s
0.0096s
6.1x
5,000
0.3541s
0.0190s
18.6x
Below about a hundred blocks the collection is marginally slower, since the array setup costs more than a handful of rectangles.
Proposal
Draw rectangle blocks as a single PolyCollection built from an (n, 4, 2) vertex array, above a block-count threshold. Keep the current per-Rectangle path below it.
A threshold rather than a straight swap, for one reason: a collection leaves ax.patchesempty. Nothing in the README or the documentation promises otherwise, but drawing the blocks as individual patches is observable, and someone may well be reaching into ax.patches to tweak a chart after the fact. That is plausible for a chart of 50 blocks and implausible for one of 50,000, which is exactly where the threshold falls anyway. MAX_BLOCKS already sets the precedent for a module-level tunable.
The three awkward cases all work
Checked against a prototype rather than assumed:
Per-block colours map to the collection's facecolors array.
Fractional blocks work: rounding_rule="float" emits partial and split cells, and a PolyCollection takes arbitrary quads, so a half-width block is just different vertices rather than a special case.
block_edge_color / block_edge_width become the edgecolors and linewidths arrays.
Out of scope
icons and characters draw Text, not patches, so they gain nothing here. They are the slower case, since each glyph is laid out individually, and would need a different approach entirely.
Safety net
The rendered output must not change at all below the threshold, and must not change perceptibly above it. tests/test_images.py --mpl covers this directly. The geometry fingerprint used when _make_single_waffle was split up applies too: render a few hundred parameter combinations and compare every block's position, size, face colour, edge colour and line width before and after.
The existing suite reads ax.patches in 33 places across 7 files, so those tests need a helper that reads blocks from whichever representation is in use. That is mechanical, but it is the bulk of the diff.
Worth doing?
Yes, but not urgently, and not as part of a feature release. It changes how every chart is rendered for a benefit most users will never see. The payoff is that charts in the thousands of blocks stop being slow, #2 gets a better answer than "use a vector format", and MAX_BLOCKS could be raised on evidence rather than as a guess.
_draw_blockscreates onematplotlib.patches.Rectangleper block and adds it withax.add_artist. matplotlib then transforms and draws each one individually, so both building and drawing scale linearly with an expensive per-block constant.For a normal chart of tens or hundreds of blocks this is invisible and not worth changing. It matters for the thousand-plus-block charts that #2 was about, and it is the reason
MAX_BLOCKSexists as a backstop rather than as a considered limit.Measured
Three categories, one chart, timed as build plus
canvas.draw(), best of five:PatchCollectionPolyCollectionfrom a vertex arraySplit into build and draw at 50,000 blocks:
PatchCollectionPolyCollectionA
PatchCollectionfixes drawing but not building, because constructing 50,000Rectangleobjects in Python is what dominates once drawing is cheap. Building the vertex array directly, with no per-block Python object at all, is what removes the rest. 29x faster overall at 50,000 blocks.Where it starts to pay
PolyCollectionBelow about a hundred blocks the collection is marginally slower, since the array setup costs more than a handful of rectangles.
Proposal
Draw rectangle blocks as a single
PolyCollectionbuilt from an(n, 4, 2)vertex array, above a block-count threshold. Keep the current per-Rectanglepath below it.A threshold rather than a straight swap, for one reason: a collection leaves
ax.patchesempty. Nothing in the README or the documentation promises otherwise, but drawing the blocks as individual patches is observable, and someone may well be reaching intoax.patchesto tweak a chart after the fact. That is plausible for a chart of 50 blocks and implausible for one of 50,000, which is exactly where the threshold falls anyway.MAX_BLOCKSalready sets the precedent for a module-level tunable.The three awkward cases all work
Checked against a prototype rather than assumed:
facecolorsarray.new-linepadding works: alpha 0 in bothfacecolorsandedgecolors, which preserves the fix from Don't outline the blank cells that new-line padding leaves behind #62 where padding must not pick up a visible border.rounding_rule="float"emits partial and split cells, and aPolyCollectiontakes arbitrary quads, so a half-width block is just different vertices rather than a special case.block_edge_color/block_edge_widthbecome theedgecolorsandlinewidthsarrays.Out of scope
iconsandcharactersdrawText, not patches, so they gain nothing here. They are the slower case, since each glyph is laid out individually, and would need a different approach entirely.Safety net
The rendered output must not change at all below the threshold, and must not change perceptibly above it.
tests/test_images.py --mplcovers this directly. The geometry fingerprint used when_make_single_wafflewas split up applies too: render a few hundred parameter combinations and compare every block's position, size, face colour, edge colour and line width before and after.The existing suite reads
ax.patchesin 33 places across 7 files, so those tests need a helper that reads blocks from whichever representation is in use. That is mechanical, but it is the bulk of the diff.Worth doing?
Yes, but not urgently, and not as part of a feature release. It changes how every chart is rendered for a benefit most users will never see. The payoff is that charts in the thousands of blocks stop being slow, #2 gets a better answer than "use a vector format", and
MAX_BLOCKScould be raised on evidence rather than as a guess.