pythonmatplotlibscatter-plot

Cut off scatterplot markers in matplotlib after autoscale


I'm generating a simple image with a scatter and two dots and I want the dots to be fully visible, but ax.autoscale_view() seems not to work properly.

fig, ax = plt.subplots(figsize=(3, 3), dpi=150)
ax.scatter([0, 1], [0, 1], s=2000)
ax.autoscale_view()

scatterplot

Am I doing something wrong? How can I get the dots to be fully visible no matter the marker size s?


Solution

  • To adjust the axis so that the perimeter of points is inside of the plotting area, we need to:

    The size parameter s in ax.scatter sets the area of the bounding box (a square) of the marker (the dot) in units of points^2. The marker also has a line drawn around it, controlled by the parameter linewidth. The default is 1.0. To get the radius in points and the linewidth in points:

    fig, ax = plt.subplots(figsize=(3,3), dpi=150)
    c = ax.scatter([0, 1], [0, 1], s=2000)
    
    r_face = sqrt(2000) / 2     # = 22.36 points
    lw = c.get_linewidth()[0]   # = 1.0 points
    r_points = r_face + lw      # = 23.36 points
    

    To convert to pixel-coordinate-space, we need to convert from points to inches (72 points per inch), and then multiply by the DPI.

    r_pixel = r_points / 72 * 150  # = 48.67 pixels
    

    You can get the center of a point (we will use the point at 1, 1) and the boundary extent in pixel-coordinate-space using:

    x_pixel, y_pixel = ax.transData.transform((1, 1))
    bx_pixel = x_pixel + r_pixel
    by_pixel = y_pixel + r_pixel
    

    Because the dot radius is fixed in pixel-coordinate-space, it does not scale 1-to-1 with the data coordinates. This makes a direct calculation difficult (I believe it is possible, I just don't know how). We can iteratively adjust the margins and calculate whether the boundary is within the bounds of the data-coordinates.

    bx_data, by_data = ax.transData.inverted().transform((bx_pixel, by_pixel))
    xmin, xmax = ax.get_xlim()
    ymin, ymax = ax.get_ylim()
    while (bx_data > xmax) or (by_data > ymax):
        margin_x, margin_y = ax.margins()
        margin_x += 0.05
        margin_y += 0.05
        ax.margins(margin_x, margin_y)
        x_pixel, y_pixel = ax.transData.transform((1, 1))
        bx_pixel = x_pixel + r_pixel
        by_pixel = y_pixel + r_pixel
        bx_data, by_data = ax.transData.inverted().transform((bx_pixel, by_pixel))
        xmin, xmax = ax.get_xlim()
        ymin, ymax = ax.get_ylim()
    ax.autoscale()
    

    enter image description here