How to adjust the marker size of a scatter plot, so that it matches a given radius? (Using matplotlib transformations)
Asked Answered
L

1

5

I want to have the markers of a scatter plot match a radius given in the data coordinates.

I've read in pyplot scatter plot marker size, that the marker size is given as the area of the marker in points^2.

I tried transforming the given radius into points via axes.transData and then calculating the area via pi * r^2, but I did not succeed.

Maybe I did the transformation wrong. It could also be that running matplotlib from WSL via VcXsrv is causing this problem.

Here is an example code of what I want to accomplish, with the results as an image below the code:

import matplotlib.pyplot as plt
from numpy import pi

n = 16
# create a n x n square with a marker at each point
x_data = []
y_data = []
for x in range(n):
    for y in range(n):
        x_data.append(x)
        y_data.append(y)

fig,ax = plt.subplots(figsize=[7,7])

# important part:
# calculate the marker size so that the markers touch

# radius in data coordinates:
r = 0.5 
# radius in display coordinates:
r_ = ax.transData.transform([r,0])[0] - ax.transData.transform([0,0])[0]
# marker size as the area of a circle
marker_size = pi * r_**2

ax.scatter(x_data, y_data, s=marker_size, edgecolors='black')

plt.show()

When I run it with s=r_ I get the result on the left and with s=marker_size I get the result on the right of the following image: Example

Living answered 6/12, 2020 at 23:57 Comment(0)
R
7

The code looks perfectly fine. You can see this if you plot just 4 points (n=2):

4 cricles

The radius is (almost) exactly the r=0.5 coordinate-units that you wanted to have. wait, almost?! Yes, the problem is that you determine the coordinate-units-to-figure-points size before plotting, so before setting the limits, which influence the coordinate-units but not the overall figure size...

Sounded strange? Perhaps. The bottom line is that you determine the coordinate transformation with the default axis-limits ((0,1) x (0,1)) and enlarges them afterwards to (-0.75, 15.75)x(-0.75, 15.75)... but you are not reducing the marker-size.

So either set the limits to the known size before plotting:

ax.set_xlim((0,n-1))
ax.set_ylim((0,n-1))

The complete code is:

import matplotlib.pyplot as plt
from numpy import pi

n = 16
# create a n x n square with a marker at each point as dummy data
x_data = []
y_data = []
for x in range(n):
    for y in range(n):
        x_data.append(x)
        y_data.append(y)

# open figure
fig,ax = plt.subplots(figsize=[7,7])
# set limits BEFORE plotting
ax.set_xlim((0,n-1))
ax.set_ylim((0,n-1))
# radius in data coordinates:
r = 0.5 # units
# radius in display coordinates:
r_ = ax.transData.transform([r,0])[0] - ax.transData.transform([0,0])[0] # points
# marker size as the area of a circle
marker_size = pi * r_**2
# plot
ax.scatter(x_data, y_data, s=marker_size, edgecolors='black')

plt.show()

output set limits

... or scale the markers's size according to the new limits (you will need to know them or do the plotting again)

# plot with invisible color
ax.scatter(x_data, y_data, s=marker_size, color=(0,0,0,0))
# calculate scaling
scl = ax.get_xlim()[1] - ax.get_xlim()[0]
# plot correctly (with color)
ax.scatter(x_data, y_data, s=marker_size/scl**2, edgecolors='blue',color='red')

This is a rather tedious idea, because you need to plot the data twice but you keep the autosizing of the axes...

output with double plotting

There obviously remains some spacing. This is due a misunderstanding of the area of the markers. We are not talking about the area of the symbol (in this case a circle) but of a bounding box of the marker (imagine, you want to control the size of a star or an asterix as marker... one would never calculate the actual area of the symbol). So calculating the area is not pi * r_**2 but rather a square: (2*r_)**2

# open figure
fig,ax = plt.subplots(figsize=[7,7])
# setting the limits
ax.set_xlim((0,n-1))
ax.set_ylim((0,n-1))
# radius in data coordinates:
r = 0.5 # units
# radius in display coordinates:
r_ = ax.transData.transform([r,0])[0] - ax.transData.transform([0,0])[0] # points
# marker size as the area of a circle
marker_size = (2*r_)**2
# plot
ax.scatter(x_data, y_data, s=marker_size,linewidths=1)
#ax.plot(x_data, y_data, "o",markersize=2*r_)
plt.show()

output (2*r)**2 As soon as you add an edge (so a non-zero border around the markers), they will overlap: overlap

If even gets more confusing if you use plot (which is faster if all markers should have the same size as the docs state as "Notes"). The markersize is only the width (not the area) of the marker:

ax.plot(x_data, y_data, "o",markersize=2*r_,color='magenta')

plot command

Rookie answered 7/12, 2020 at 7:52 Comment(5)
With setting the limits beforehand it works better but still not perfect. There are gaps in the pictures you posted. And if I run the exact code you posted I get this: imgur.com/a/YZGIFKSLiving
@NicoG. odd that you have an overlap where I didn't had one. Anyway, I searched the docs and found the solution, you need: it is the area of the marker, not of the symbol. And this area always is a square (I added the explanation & examples)Rookie
That makes sense. You made a spelling mistake. You wrote (r*r_)**2 instead of (2*r_)**2 in the explanation. If I try this in a Jupyter Notebook I get the same result that you got. I still notice a difference in the x and y direction, but thats barely noticable. But running it as a python file via WSL+VcXsrv I still get a wrong result. Since pi=3.14 and 2*2=4 my new result has even more overlap. It could be that matplotlib does not get the right display coordinates, because it's a virtual machine. It could also be that plt.show() does some scaling, that I cannot account for.Living
good eyes, thx! I ran this as normal python-file with matplotlib 3.3.1 (in VScode on Windows). Seems to be odd that you get a different appearanceRookie
I somehow fixed it by multiplying the marker size with 0.5 . With this new information you gave me and the strange correction factor, it works in this example as well as in the actual project I wanted this code for. I'm really curious and confused why this is. Thx for your answers!Living

© 2022 - 2024 — McMap. All rights reserved.