Performance and Large Data

What makes a figure slow, and what to do when there are more points than pixels.

Overview

Artists, not data

matplotlib can draw a million points in under a second. It can also spend thirty seconds drawing a hundred lines if you create them the wrong way. The difference is not the amount of data. It is the number of artist objects the library has to manage.

Understanding what makes matplotlib slow -- and what does not -- lets you avoid performance problems without rewriting your plotting code. Most slowdowns come from one of three patterns: creating many small artists in a loop, using scatter when plot would do, and saving at an unnecessarily high DPI. Each has a simple fix.

This page covers the artist-count model, the difference between plot and scatter in performance, collection objects that draw many items as one artist, file-size control, animation performance, draw-time measurement and the cases where matplotlib is the wrong tool and a GPU-based library is needed.

The audience for performance tuning is smaller than for other topics. If your charts draw instantly and save quickly, you do not need this page. It becomes useful when you have large datasets, many panels, or a production pipeline that generates hundreds of figures. At that point, the difference between a one-artist line and a thousand-artist loop is the difference between a usable script and one that needs a progress bar.

The page also covers the first-run font-cache cost, which makes the first figure in a session take several seconds. That delay is not a performance problem with your code. It happens once and does not recur.

Knowing when to stop optimising is part of the skill. A chart that takes half a second to draw and will be viewed once does not need optimisation. A chart in an interactive dashboard that redraws on every slider change does. The page ends with a decision framework for when performance work is worth the effort.

matplotlib's cost is dominated by the number of artists it manages, not the number of data values.

One plot call with a million points creates one Line2D. A loop making two thousand calls creates two thousand artists, each with its own properties, transform and draw pass. And that is far slower even with a fraction of the data.

The rule that follows is the same one as NumPy and pandas: pass arrays, don't loop. Where a loop is unavoidable, LineCollection and its relatives draw many segments as a single artist.

Worth knowing

Cost grows with the number of artists, not data points — one call with arrays beats a loop of calls by a wide margin.
plot with a marker is faster than scatter when size and colour do not vary.
Past a few thousand points there are more points than pixels, and the extras cannot be seen at all.
Subsampling drops genuine spikes; binning to a min/max envelope keeps them.
rasterized=True stores one artist as pixels while text and axes stay vector — a much smaller PDF with sharp labels.
Creating a figure is the expensive part of a drawing loop; reuse it with set_ydata, or at least close it.

Performance and Large Data

What makes a figure slow. What to do when there are more points than pixels.

Drawing cost grows with artists, not data

One line of a million points is fast; a million lines is not.

example_01.pymatplotlib
Output

scatter versus plot for many points

The uniform case is faster, because there is less to vary.

example_02.pymatplotlib
Output

More points than pixels

Beyond a certain n, the extra points cannot be seen at all.

example_03.pymatplotlib
Output

A seven-inch figure at 100 dpi is 700 pixels wide. Two hundred thousand points is roughly 285 per pixel column. 284 of them are drawn just on top of others.

They cost time and file size and contribute nothing visible.

Reducing them isn't a compromise. It's dropping a thing the reader could never see. Two approaches, and the gap matters.

Subsampling — x[::step] — is trivial and drops what it doesn't land on. A single-sample spike disappears. For smooth, dense data that is fine. For anything where extremes matter it isn't.

Aggregating — binning by x and drawing the min and max in each bin as a filled band — keeps the envelope. The chart looks nearly same to the full data because the envelope is what the eye was reading anyway, and genuine spikes live.

The fourth editor puts one real spike in the data and shows subsampling losing it while binning keeps it.

For scatter data, the equal is hexbin or hist2d. This Replaces overplotted points with measured density.

Aggregate rather than sample

Binning preserves the extremes that sampling drops.

example_04.pymatplotlib
Output

Rasterising a layer

Keeps a vector file small while leaving the text sharp.

example_05.pymatplotlib
Output

Loops that draw

The cost is per figure, and the fix is to reuse or close.

example_06.pymatplotlib
Output

plot versus scatter

For many points that all look alike, ax.plot(x, y, "o") is faster than ax.scatter(x, y).

scatter builds a set that carries a size and colour per point. That is what it's for. When those don't vary, the machinery is unused and paid for anyway.

So: scatter when size or colour codes a thing, plot when it doesn't.

File size

Vector formats store each element. So A scatter of 30,000 points is 30,000 objects in the PDF. Such files are slow to open and large.

rasterized=True on the artist stores that layer as pixels while all else. Axes, ticks, labels, title — stays vector:

ax.scatter(x, y, s=2, rasterized=True)
fig.savefig("out.pdf", dpi=200)

The result is a small file with sharp text and a pixel-based data layer. This Is just the right trade for a dense scatter in a doc. The dpi at save time controls the detail of the rasterised part.

Drawing loops

making many similar charts — one per group, per day, per file — spends most of its time creating figures, not drawing data.

Two levels of fix.

Close each figure: plt.close(fig). This is not an optimisation so much as a requirement; without it the figures accumulate, matplotlib warns after twenty, and memory grows without bound.

Reuse the figure: create it once, update the artist's data each iteration, and re-save:

line.set_ydata(new_y)
ax.relim(); ax.autoscale_view()
fig.savefig(path)

relim and autoscale_view are needed because setting data doesn't rescale the axes.

This is usefully faster when the layout is same between charts. It's the same method animations use.

When matplotlib is the wrong tool

matplotlib renders once and isn't built for interactive exploration of large data.

Datashader aggregates hundreds of millions of points into an image. Is the right answer above a few million.

Plotly, Bokeh, Altair give pan-and-zoom in a browser. This Matplotlib's interactive backends do only awkwardly.

mpl-scatter-density and similar handle the dense-scatter case within matplotlib.

The signal is often the size: if a figure takes more than a few seconds to draw, the answer is a other approach rather than a faster matplotlib.

Measuring draw time

Timing a matplotlib figure needs care. The drawing is lazy. Creating artists is fast and nothing is rendered until the canvas is drawn.

t = time.perf_counter()
fig.canvas.draw()
elapsed = time.perf_counter() - t

Without the clear draw(), the timing measures only object creation and reports numbers far too good.

The first draw of a session also pays for font cache and backend init. This Can be a second or more. A warm-up figure before timing anything is the gap between a real compare and a misleading one. The editors in this module do just that.

Where the time goes

For a common figure, in rough order:

Artist creation when there are many artists.

Rendering the artists to pixels.

Text layout, which is more costly than it sounds. A chart with hundreds of tick labels or annotations spends real time measuring glyphs.

Font cache building, once per setting, and at times minutes on first run.

Saving, above all to vector formats with many elements.

The hands-on results: fewer artists, fewer text objects, and rasterised layers for dense data.

Interactive versus file output

An interactive backend redraws on each pan, zoom and resize. A figure that takes two seconds to draw is unusable interactively while being just right fine as a file.

That changes the trade-off. For a saved figure, drawing a million points slowly once is acceptable. For a thing a person will change, the point budget is far smaller. Downsampling isn't an tuning but a need.

fig.canvas.draw_idle() defers a redraw until the event loop is free. This Is what interactive tools use to stay responsive.

Animations

FuncAnimation re-renders a figure per frame. So The per-frame cost is what matters.

blit=True redraws only the artists that changed. This Is much faster and needs the update function to return them.

The same reuse principle as the drawing loop applies. Create the artists once, update their data each frame with set_data. Never call plot inside the update.

The wider picture

matplotlib is designed for correctness and control rather than throughput. Its limits are reached sooner than people expect. Tens of thousands of artists, or a few million points.

The escape routes, roughly by problem:

Too many points — aggregate, or use datashader.

Too many artists — sets instead of loops.

Too slow interactively — a browser-based library.

Too many figures — reuse, and close.

The one that matters most is the first. Most speed problems in matplotlib are really a display problem. Drawing more than the reader can see. This Costs time and sends nothing.

Font cache and first-run cost

A surprise on a fresh setting: the very first matplotlib figure can take a long time, at times minutes, while the font cache is built.

It happens once per setting and is cached later. It's a common source of "matplotlib is incredibly slow" reports from people who ran it once in a new holder.

In a holder or CI image, drawing one throwaway figure at build time moves the cost out of the first real run.

matplotlib.get_cachedir() shows where it lives, and deleting it forces a rebuild. Which is the fix when a newly installed font isn't being found.

A performance checklist

When a figure is slow, in the order worth checking:

How many artists? len(ax.lines) + len(ax.sets) + len(ax.patches). Hundreds is fine. Tens of thousands is the problem.

How many points, against how many pixels? If it's more than a few per pixel column, the extras are invisible.

Is it in a loop? Are figures being closed?

Is it text? Hundreds of annotations or tick labels cost real time.

Is it the save? Vector output of dense data.

Is it the first run? The font cache.

Most slow figures are the first two. Both are fixed by drawing less rather than by drawing faster.

Budgets

Rough numbers, which are more handy than general advice.

Artists: up to a few thousand is comfortable. Tens of thousands is slow. Hundreds of thousands will not finish in a reasonable time.

Points in one artist: a million is fine for a line, because it is one object. The renderer handles it far better than a million separate objects.

Visible resolution: a 7-inch figure at 100 dpi has 700 columns. Above a few thousand points, most are invisible.

Text objects: hundreds are noticeable; thousands dominate.

Interactive redraw: anything above about 100 ms per draw feels sluggish when panning.

Vector output: above roughly 10,000 elements the file becomes slow to open.

Those thresholds explain most of the hands-on advice. Pass arrays rather than looping, aggregate before plotting, rasterise dense layers, and reuse figures in loops.

Performance and Large Data, in summary

Cost is driven by artists, not data points. One call with an array beats a loop of calls by a wide margin.

plot beats scatter when the markers are uniform.

Past a few thousand points there are more points than pixels. The extras cost time while communicating nothing.

Subsampling drops genuine extremes. Binning to a min/max envelope keeps them, and looks the same.

rasterized=True keeps a vector file small while leaving the text sharp.

Creating figures is the costly part of a drawing loop. Reuse them. And close them even so, because matplotlib won't.

And when a figure takes seconds to draw, the answer is often a other approach rather than a faster matplotlib.

When to stop optimising

A figure that takes two seconds to draw once. Is then saved, is fine. There's nothing to fix.

The cases that justify effort are narrower than they look:

A figure regenerated often — in a dashboard build, a CI job, a loop over thousands of groups. The cost times.

An interactive figure, where each pan and zoom pays it again.

A figure that doesn't finish, or exhausts memory. This Is a correctness problem rather than a speed one.

A file too large to open. This Is the vector-scatter case.

Outside those, drawing time is often a rounding error next to the data work that preceded it. The effort is better spent on whether the chart says the right thing.

That is worth stating because speed is easy to optimise and easy to over-optimise. A fast chart no one can read isn't an gain.

One more thing

fig.canvas.draw_idle() requests a redraw at the next opportunity rather than at once. This Is what keeps an interactive figure responsive when a few things change at once.

For a script it makes no gap. The draw happens at save time either way. It matters in a callback that updates a few artists. Here An direct draw per change would render the figure a few times for one logical update.

Performance and Large Data: the short version

Nearly each speed problem in matplotlib is a display problem in disguise: drawing more than the output can show.

Fixing it by drawing less. Aggregating, subsampling with care, rasterising a dense layer — makes the chart faster and often more readable also. This Is unusual among optimisations.

Questions people ask about Performance and Large Data

Why is my plot slow? Usually too many artists, such as a loop that makes thousands of lines, or too many points to be seen on screen.

How do I plot a million points? Downsample, aggregate into bins, use hexbin, or draw one plot call with an array rather than many calls.

Is scatter slower than plot? Yes, for large arrays. plot with a marker and no line draws one artist, while scatter tracks per-point properties.

What does rasterized=True do? It draws that artist as pixels inside a vector file, which keeps PDF and SVG sizes small.

How do I speed up animation? Update artist data with set_data instead of redrawing, and use blitting where the backend supports it.

How do I find out what is slow? Time the plotting calls with time.perf_counter or cProfile. Often the saving step, not drawing, dominates.

Reading the code back

speed work here is a short list checked in order. Is there a loop creating artists, are there more points than pixels, are figures being closed, is the output vector with a dense layer in it. Four questions. The first two account for nearly all. None of them needs knowing anything about how matplotlib renders.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What dominates matplotlib's drawing cost?

  2. When is `plot(x, y, 'o')` faster than `scatter`?

  3. Why is subsampling risky for a dense series?

  4. What does `rasterized=True` do to a PDF?

Cheat sheet

Performance and Large Data

Speed up slow matplotlib figures that render too many points. Use rasterisation, line simplification and collections to stay responsive.

MATPLOTLIB · vizlearn.in/matplotlib/performance.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.