matplotlib

Matplotlib is a Python plotting library that organizes every visualization as a hierarchy of Artist objects—Figure containing one or more Axes, each Axes containing Line2D, Patch, Text, and Collection artists—and renders that hierarchy to a pixel buffer or vector format by dispatching to a pluggable backend, with the Agg rasterizer (Anti-Grain Geometry) producing PNG output and the Cairo backend producing PDF, SVG, and PostScript. ```svg Matplotlib Artist Hierarchy Figure → Axes → Artists — every drawn element is an Artist with style properties Figure (canvas, DPI, size) fig = plt.figure(figsize=(8,6), dpi=150) → 1200×900 px raster Axes (coordinate system + spine + tick) Line2D artist xlabel (Text artist) YTick (Line2D + Text) Axes (independent coords) PathCollection artist (scatter) each point = 1 Path → 1M pts = 8 s render Backend dispatch — savefig() routes to renderer Agg → PNG/JPEG raster, ~180 KB Cairo → PDF/SVG/PS vector, ~45 KB SVG TkAgg/Qt5Agg → GUI window interactive, pan/zoom pgf → LaTeX/PGF publication-quality plt.close(fig) mandatory — circular Artist refs prevent Python GC from releasing figure memory ``` **Matplotlib's object model separates the act of building a visualization from the act of rendering it: every call to `ax.plot()`, `ax.scatter()`, or `ax.text()` creates an Artist object and adds it to the Axes container, and no pixels are produced until `fig.savefig()` or `plt.show()` triggers a renderer pass over the full hierarchy.** This deferred rendering means the same Figure object can be sent to multiple backends—Agg for a 1,200 × 900 PNG at 150 DPI, Cairo for a scalable SVG at 45 KB, or a GUI TkAgg window for interactive pan and zoom—without rebuilding any artists. The tradeoff is that every property change (color, linewidth, alpha) before `savefig` is free, but changes after rendering require a full re-render; animations work by mutating artist properties between frames rather than rebuilding the Figure. **Figure memory is managed through explicit close calls, not Python's garbage collector, because matplotlib maintains internal global state that creates reference cycles among Figure, Axes, and Canvas objects.** Calling `plt.savefig()` without `plt.close()` in a loop that generates 1,000 figures accumulates all of them in memory; at roughly 2 MB per Figure (8 × 6 in, 100 DPI, Agg), this produces a 2 GB leak invisible to `del fig`. The correct pattern is `plt.close(fig)` immediately after saving, or using `matplotlib.use('Agg')` and the `Figure` class constructor directly rather than the `plt` state machine, which registers every Figure in a global `_pylab_helpers.Gcf` dictionary. **The pyplot state machine (`plt.plot`, `plt.xlabel`) is a convenience layer over the object-oriented API that automatically creates and tracks the current Figure and Axes—but it becomes a source of subtle bugs in code that creates multiple figures or runs in parallel.** Every `plt.plot()` call dispatches to `plt.gca().plot()` (get-current-axes), where "current" is a global variable updated by `plt.figure()`, `plt.subplot()`, and `plt.axes()`. In a Jupyter notebook that re-executes cells out of order, or in a threaded server rendering multiple requests concurrently, the global current-axes pointer can be wrong. The object-oriented pattern—`fig, ax = plt.subplots()` followed by `ax.plot()`—eliminates this ambiguity entirely; every operation targets an explicit Axes object. **Scatter plots with more than 100,000 points are the most common matplotlib performance trap because `plt.scatter` renders each point as a separate Path artist, and 1,000,000 points take approximately 8 seconds to render via the Agg backend.** The PathCollection artist underlying scatter stores one Path per marker, making per-point color and size trivially addressable but rendering cost O(N) in Python artist traversal before any rasterization. For large-N scatter, the alternatives are `ax.plot(x, y, '.', markersize=1)` which renders all points as a single Line2D artist at ~300 ms, or Datashader which rasterizes 1,000,000 points in ~160 ms by aggregating into a fixed-resolution pixel grid without individual-point artists at all—a 50× speedup over `plt.scatter`. **The rcParams system gives matplotlib approximately 200 configurable style parameters—figure size, DPI, font family, line width, color cycle, tick direction, spine visibility—that can be overridden globally via `matplotlib.rcParams`, per-session via `plt.style.use()`, or per-block via the `plt.rc_context()` context manager.** A call to `plt.style.use('seaborn-v0_8')` atomically overrides roughly 40 rcParams to match Seaborn's aesthetics, while `plt.rcParams['axes.spines.top'] = False` surgically removes a single element. The `matplotlibrc` file (typically at `~/.config/matplotlib/`) applies defaults to every matplotlib session without any in-code configuration—the canonical way to enforce consistent figure size and font family across a project. **FuncAnimation renders each frame by calling an update function that mutates existing artists, then rasterizes the result via the Agg backend and hands the pixel buffer to ffmpeg for encoding.** A 100-frame animation at 30 fps produces a 3.3-second video; frame generation cost is dominated by the number of artists updated per frame, not the resolution—updating a single Line2D's data array with `set_data()` costs ~0.5 ms, while clearing and redrawing 1,000 artists costs ~50 ms, capping the animation at 20 fps before encoding even begins. The encoded MP4 from ffmpeg at quality CRF 23 typically reaches ~800 KB for a 3-second 1280×720 clip. Using `blit=True` instructs FuncAnimation to re-render only the bounding boxes of changed artists rather than the entire Figure, reducing per-frame cost by 3–10× for typical animations with a static background. | Backend | Output format | File size (8×6 in plot) | Use case | |---|---|---|---| | Agg | PNG, JPEG | ~180 KB PNG | Reports, notebooks, CI | | Cairo | PDF, SVG, PS | ~45 KB SVG | Publications, web | | TkAgg / Qt5Agg | GUI window | — | Interactive exploration | | pgf | LaTeX PGF/TikZ | — | Academic papers | ``` MATPLOTLIB RENDER FLOWCHART ax.plot() / ax.scatter() / ax.text() │ ▼ ┌─────────────────────┐ │ Artist created │ Line2D / PathCollection / Text │ added to Axes │ no pixels yet — deferred render └────────┬────────────┘ │ ▼ ┌─────────────────────┐ │ fig.savefig() or │ triggers renderer pass │ plt.show() │ traverses Artist hierarchy └────────┬────────────┘ │ ┌─────┴──────┐ │ │ Agg backend Cairo/pgf │ │ Rasterize Vectorize to px buffer to commands │ │ PNG/JPEG SVG/PDF/PS │ plt.close(fig) ← mandatory: clears Gcf registry ``` Read matplotlib through a *deferred Artist hierarchy* lens rather than a *draw-commands* lens. Every `ax.plot()` call is an instruction to build an object, not to draw a line; the renderer that actually produces pixels or vectors is invoked only at save-time and knows nothing about how artists were constructed. That separation is why rcParams, style sheets, and backend swaps can all modify the final output without touching any plot code—and why a `plt.close()` after every `savefig()` is not optional hygiene but a required cleanup of the persistent object graph that matplotlib maintains across the entire session. It also explains why Seaborn, Plotly, and Altair coexist with matplotlib rather than replacing it: they operate at a higher abstraction level (statistical encoding, grammar of graphics) but ultimately produce matplotlib Artists or independent scene graphs—the deferred hierarchy remains the universal lowest-common-denominator rendering contract in the Python visualization ecosystem.

Go deeper with CFSGPT

Get AI-powered deep-dives, save terms, and run advanced simulations — free account.

Create Free Account