# mpl-scatter-density replaces the scatter call with a binned image

> mpl-scatter-density is a small Matplotlib add-on that renders a density map from millions of points by binning them instead of drawing markers, with an interactive mode that downsamples while you pan. It suits anyone whose scatter plot has stopped opening, and it is wrong when you need to see individual outliers.

**astrofrog/mpl-scatter-density** — :zap: Fast scatter density plots for Matplotlib :zap:

- Repository: https://github.com/astrofrog/mpl-scatter-density
- Stars: 526 · Forks: 30
- Language: Python
- License: BSD-2-Clause
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/astrofrog-mpl-scatter-density

## Why the projection exists: markers stop working at scale

mpl-scatter-density answers a specific complaint. The README opens with it: plotting millions of points can be slow, really slow, and asks why not use density maps instead. The package description calls itself fast scatter density plots for Matplotlib.

The technical answer is that a scatter plot draws one marker per observation, so the cost of the figure scales with the number of points in the data rather than with the number of pixels on screen. A density map bins the points into a grid first and then draws one image, so the cost of rendering stops growing with the dataset.

The README claims the animation shows real-time interactive use with 10 million points, and that interactive performance is still good with 100 million points, more if you have enough RAM. It also notes a detail that shows the design is interactive first: when panning, the density map is shown at a lower resolution to keep things responsive, and that behaviour is customizable.

The package is small and focused. It has one dependency chain worth knowing: numpy, matplotlib, and fast-histogram, which is the author's own histogram library and the piece doing the binning work.

## Installing a package that pulls in three dependencies

Installation is a single pip command, and the README states it plainly:

```bash
    pip install mpl-scatter-density
```

The dependencies are numpy, matplotlib and fast-histogram, and the README says they will be installed by pip if they are missing, so a fresh environment is enough to get started.

Two details in that paragraph are worth flagging before you commit. The README states that both Python 2.7 and Python 3.x are supported, which tells you the codebase predates the Python 2 removal and has not been rewritten since, and that the package should work correctly on Linux, MacOS X and Windows.

For a package that people add to an existing scientific environment, the dependency version question is the one that actually matters, and the README is quiet on it. The README does not state which Matplotlib versions are tested, and it does not mention the fast-histogram version either. Given the last push was on 2026-04-16, the package is a snapshot of an API from that date rather than something that follows Matplotlib releases.

## Ten million points through one extra keyword argument

The easy path is to import the module and add a projection option when you create the axes. The README's example generates ten million normally distributed points and plots them:

```python
    import numpy as np
    import mpl_scatter_density
    import matplotlib.pyplot as plt

    # Generate fake data

    N = 10000000
    x = np.random.normal(4, 2, N)
    y = np.random.normal(3, 1, N)

    # Make the plot - note that for the projection option to work, the
    # mpl_scatter_density module has to be imported above.

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1, projection='scatter_density')
    ax.scatter_density(x, y)
    ax.set_xlim(-5, 10)
    ax.set_ylim(-5, 10)
    fig.savefig('gaussian.png')
```

Three things in that snippet carry the design. The projection keyword is 'scatter_density', and the comment insists that mpl_scatter_density has to be imported above, because the projection is registered by the import rather than by the package being installed. The axes you get back is a ScatterDensityAxes instance, which has the usual methods such as scatter and plot, plus scatter_density.

The axes are otherwise a normal Matplotlib axes, which is why set_xlim and set_ylim work exactly as you would expect, and why the result saves with a normal fig.savefig call.

## The scatter_density method borrows imshow's options wholesale

The scatter_density method takes the same options as imshow, which the README lists as cmap, alpha and norm among others. That inheritance is the reason the package composes with the rest of Matplotlib: the density map is an image, so the entire image styling vocabulary applies to it.

Three extra arguments sit on top of that.

The dpi argument sets the resolution of the density map. It is an integer, 72 by default, and you can set it to None to use the default for the Matplotlib backend you are using. This is the single most important knob for a big dataset, since resolution trades bin size against memory and render time.

The downres_factor argument controls how much the density map is downsampled when panning in interactive mode, and setting it to 1 disables downsampling entirely. That is the setting to change if you are working in a notebook and would rather have the full resolution map at all times, or if you are zooming rather than panning.

The color argument takes any valid matplotlib color and builds a monochromatic colormap from it that fades to transparent, which the README says makes this mode ideal when showing multiple density maps together.

## Overlaying two densities with a fading monochromatic map

The color option is the feature that turns a density plot into a comparison, because a monochrome map that fades to transparent lets two maps occupy the same axes without one hiding the other:

```python
    import numpy as np
    import matplotlib.pyplot as plt
    import mpl_scatter_density  # noqa

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1, projection='scatter_density')

    n = 10000000

    x = np.random.normal(0.5, 0.3, n)
    y = np.random.normal(0.5, 0.3, n)

    ax.scatter_density(x, y, color='red')

    x = np.random.normal(1.0, 0.2, n)
    y = np.random.normal(0.6, 0.2, n)

    ax.scatter_density(x, y, color='blue')

    ax.set_xlim(-0.5, 1.5)
    ax.set_ylim(-0.5, 1.5)

    fig.savefig('double.png')
```

Two calls to scatter_density on the same axes produce two overlapping maps, one red and one blue, with two ten-million-point datasets that are deliberately offset from each other so the difference is visible.

The import of mpl_scatter_density carries a noqa comment in this example. That is a small hint about the tooling: the module is imported purely for its side effect of registering the projection, and the linter would otherwise flag the unused import. The same pattern appears in the first example, where the comment above the import explains the same requirement in words.

## Log stretches and the colorbar, using astropy's Normalize

Density maps often have a high dynamic range, where a handful of pixels hold orders of magnitude more points than the rest. The README's answer is to pass a matplotlib.colors.Normalize object to the norm argument, in the same way you would for imshow, and it points at astropy for a framework that builds such objects.

```python
    import numpy as np
    import mpl_scatter_density
    import matplotlib.pyplot as plt

    # Make the norm object to define the image stretch
    from astropy.visualization import LogStretch
    from astropy.visualization.mpl_normalize import ImageNormalize
    norm = ImageNormalize(vmin=0., vmax=1000, stretch=LogStretch())

    N = 10000000
    x = np.random.normal(4, 2, N)
    y = np.random.normal(3, 1, N)

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1, projection='scatter_density')
    ax.scatter_density(x, y, norm=norm)
    ax.set_xlim(-5, 10)
    ax.set_ylim(-5, 10)
    fig.savefig('gaussian_log.png')
```

The colorbar works the same way it does for an image, because the density map is one. The README keeps the returned density object for that:

```python
    density = ax.scatter_density(x, y)
    ax.set_xlim(-5, 10)
    ax.set_ylim(-5, 10)
    fig.colorbar(density, label='Number of points per pixel')
```

The label is the useful part. It names the quantity as points per pixel, which is a direct consequence of the binned approach: the color scale is a count per bin, not a count per observation.

## ScatterDensityArtist for people who want the axes and the artist separately

The second entry point is the ScatterDensityArtist, which the README describes as being used behind the scenes in the projection example and as the option for more experienced Matplotlib users. You initialize it with the axes as the first argument, followed by whatever you would have passed to scatter_density, and then you add it to the axes yourself:

```python
    from mpl_scatter_density import ScatterDensityArtist
    a = ScatterDensityArtist(ax, x, y)
    ax.add_artist(a)
```

The distinction between the two paths is who owns the artist. With the projection, the axes object created by add_subplot is a ScatterDensityAxes and it creates the density artist for you when you call scatter_density. With the artist, you construct the artist, which means you can hold a reference to it, and you decide when it is added to the axes.

The README points at the docstring for ScatterDensityArtist for the argument list, and notes that you can pass the same arguments you would have passed to scatter_density.

The alternative to all of this is worth naming, because the package is not the only way to get a binned image. You can bin the data yourself with fast-histogram, which the package already depends on, and draw the result with imshow. That route is more work and gives you full control over the bin edges, at the cost of reimplementing what the projection already wires together.

## Maintenance: one release, a last push in April 2026

The maintenance picture needs plain statement. The repository is not archived, and the last push was on 2026-04-16. There is one tagged release, v0.8, published on 2024-12-03, and the CHANGES.md file at the repository root is where the history lives.

So the timeline has a gap in it. The most recent release is roughly a year and a half older than the last commit, and a version below 1.0 with a single tag is what a small utility looks like from the outside, whether or not the author still works on it.

The licence is BSD-2-Clause, with the LICENSE file at the root. That is a permissive licence, and for a package you import into a script the practical effect is minimal. This is not legal advice, and anyone redistributing it inside a larger product should read the file.

The repository layout is simple and gives a hint about scope: mpl_scatter_density is the package itself, examples holds example_density.py, example_double.py and example_taxi.py, images holds the figures, and the build is set up with setuptools and setuptools_scm, which means the version number is derived from the git tag rather than written by hand. The taxi example is the one the README's animation comes from, and it is a good place to look for how a real dataset is loaded.

## Conclusion

mpl-scatter-density is worth adopting when a scatter plot of your data has become unreadable or slow, when the point density is the message rather than the individual observations, and when you want the result to stay a Matplotlib figure. It is the wrong tool when a single extreme point has to remain visible, when you need hexbin or KDE semantics rather than a pixel count, and when your environment pins a Matplotlib version the package has not been checked against. Before installing, check the last push, which was on 2026-04-16, and the single tagged release v0.8 from 2024-12-03, and confirm that your Matplotlib version still exposes the projection mechanism the package relies on.

## FAQ

### How do I install mpl-scatter-density?

Run pip install mpl-scatter-density. The package requires numpy, matplotlib and fast-histogram, and the README says pip will install them if they are missing.

### Why does my scatter_density projection fail until I import mpl_scatter_density?

The projection keyword is registered by the import, not by the install. The README's example comments that the mpl_scatter_density module has to be imported above the point where the axes with projection='scatter_density' are created.

### How do I control the resolution of the density map?

Pass the dpi argument to scatter_density. It is an integer that determines the resolution of the density map, 72 by default, and setting it to None uses the default for the Matplotlib backend you are using.

### Can I show two density maps on the same axes?

Yes, by calling scatter_density twice with different color values such as 'red' and 'blue'. The color argument builds a monochromatic colormap that fades to transparent, which the README says makes the mode ideal for showing multiple density maps together.

### How do I show a log scale density map with mpl-scatter-density?

Pass a matplotlib.colors.Normalize object to the norm argument, as you would for imshow. The README shows using astropy's ImageNormalize with a LogStretch to build that object.

## Sources

- [astrofrog/mpl-scatter-density on GitHub](https://github.com/astrofrog/mpl-scatter-density)
- [Issues](https://github.com/astrofrog/mpl-scatter-density/issues)
- [License: BSD-2-Clause](https://github.com/astrofrog/mpl-scatter-density/blob/main/LICENSE)
- [README](https://github.com/astrofrog/mpl-scatter-density/blob/main/README.md)
- [Releases](https://github.com/astrofrog/mpl-scatter-density/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/astrofrog-mpl-scatter-density
