pythonmatplotlibboxplot

Matplotlib boxplots displayed with constant width


I'm plotting boxplots that are positioned very far apart on the X axis. I'd like to be able to zoom in and out on this graph and have the different boxplots displayed with the same width, independent of the level of zoom.

For comparison, I'd like to achieve something similar with what happens with markers on plot() or scatter() graphs that remain the same size on screen as one zooms in and out of the graph area.

Plotting the boxplots as is displays them as narrow when zoomed out and thicker as you zoom in. Changing the value of the widths parameter when calling boxplot() naturally only changes the width of the boxes, but they still scale with the zoom level.

Here is the code snippet and resulting output.

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(1)

# Generating random data
N = 10
y1 = np.random.randn(N)
y2 = np.random.randn(N)
y3 = np.random.randn(N)

data = [y1,y2,y3]

names = ["Near","Far","Farther"]

x_positions = [1,10,100]

fig,ax = plt.subplots()
ax.boxplot(data,positions =x_positions)
ax.set_xticklabels(names)
plt.xticks(rotation=45, ha='right')

plt.show()

Zoomed out:

enter image description here

Zoomed in:

enter image description here


Solution

  • Boxplots are drawn very simply by calling ax.plot multiple times so we end up with a dictionary containing a bunch of Line2D instances. We can make a function to update the boxes to any width (note here bp is the return value of boxplot):

    def update_boxplot_widths(width):
        for x, b, m in zip(x_positions, bp['boxes'], bp['medians']):
            left = x - width / 2
            right = x + width / 2
            b.set_xdata([left, right, right, left, left])
            m.set_xdata([left, right])
    

    To make the width a function of the axes width we can create a callback function. Here I set the boxplot width to 5% of the x-axis width:

    def boxplot_width_callback(ax):
        x_lims = ax.get_xlim()
        new_boxplot_width = (x_lims[1] - x_lims[0]) * 0.05
        update_boxplot_widths(new_boxplot_width)
    

    To have the widths automatically update, we need to add this function to the axes callbacks:

    fig, ax = plt.subplots()
    
    bp = ax.boxplot(data, positions=x_positions)
    ax.set_xticklabels(names)
    plt.xticks(rotation=45, ha='right')
    
    # Apply the desired width for the initial view.
    boxplot_width_callback(ax)
    
    # Add to axes callbacks so the width updates when we zoom.
    ax.callbacks.connect('xlim_changed', boxplot_width_callback)
    
    plt.show()
    

    Initial view:

    enter image description here

    Zoom:

    enter image description here