I have for instance the following line drawn in matplotlib
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(2,1,1) # two rows, one column, first plot
# This should be a straight line which spans the y axis
# from 0 to 50
line, = ax.plot([0]*50, range(50), color='blue', lw=2)
line2, = ax.plot([10]*100, range(100), color='blue', lw=2)
how can I get how many pixels that straight line is, in the y direction?
Note: I have several of these lines with gaps in between and I would like to put text next to them, however, if there are too many lines, I would need to know how much text I can add, that is the reason why I need the height of the line.
For instance in the attached photo, there is a blue line on the right hand side which is roughly 160 pixels in height. In a height of 160 pixels (with the font I am using) I can fit in roughly 8 lines of text as the height of the text is roughly 12 pixels in height.
How can I get the information on how tall the line is in pixels? Or is there a better way to lay the text out?
In order to obtain the height of a line in units of pixels you can use its bounding box. To make sure the bounding box is the one from the line as drawn on the canvas, you first need to draw the canvas. Then the bounding box is obtained via .line2.get_window_extent(). The difference between the upper end of the bounding box (y1) and the lower end (y0) is then the number of pixels you are looking for.
fig.canvas.draw()
bbox = line2.get_window_extent(fig.canvas.get_renderer())
# at this point you can get the line height:
print "Lineheight in pixels: ", bbox.y1 - bbox.y0
In order to draw text within the y-extent of the line, the following may be useful. Given a fontsize in points, e.g. fontsize = 12, you may calculate the size in pixels and then calculate the number of possible text lines to fit into the range of pixels determined above. Using a blended transform, where where x is in data units and y in pixels allows you to specify the x-coordinate in data units (here x=8) but the y coordinate in a coordinate in pixels calculated from the extent of the line.
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
fig = plt.figure()
ax = fig.add_subplot(2,1,1)
line, = ax.plot([0]*50, range(50), color='blue', lw=2)
line2, = ax.plot([10]*100, range(100), color='blue', lw=2)
fig.canvas.draw()
bbox = line2.get_window_extent(fig.canvas.get_renderer())
# at this point you can get the line height:
print "Lineheight in pixels: ", bbox.y1 - bbox.y0
#To draw text
fontsize=12 #pt
# fontsize in pixels:
fs_pixels = fontsize*fig.dpi/72.
#number of possible texts to draw:
n = (bbox.y1 - bbox.y0)/fs_pixels
# create transformation where x is in data units and y in pixels
trans = transforms.blended_transform_factory(ax.transData, transforms.IdentityTransform())
for i in range(int(n)):
ax.text(8.,bbox.y0+(i+1)*fs_pixels, "Text", va="top", transform=trans)
plt.show()
Related
I want to draw Circle on my plot. For this purpose I decided to use patch.Circle class from matplotlib. Cirlce object uses radius argument to set a radius of a circle, but if the axes ratio is not 1 (see my plot), how to draw circle with right proportions?
My code for drawing circle is:
rect = patches.Circle(xy=(9, yaxes),radius= 2, linewidth=3, edgecolor='r', facecolor='red',alpha=0.5)
ax.add_patch(rect)
yaxes is equal 206 in this example (because I wanted to draw it upper left coner).
Here is a picture I got using this code:
But I want something like this:
You could use ax.transData to transform 1,1 vs 0,0 and obtain the deformation in x vs y direction. That ratio can be used to know the horizontal versus the vertical size of the circle.
If you just need to place a circle using coordinates relative to the axes, plt.scatter with transform=ax.transAxes can be used. Note that the size is an "area" measure based on "points" (a "point" is 1/72th of an inch).
The following example code uses the data coordinates to position the "circle" (using an ellipse) and the x-coordinates for the radius. A red circle is placed using axes coordinates.
from matplotlib import pyplot as plt
from matplotlib.patches import Ellipse
import pandas as pd
import numpy as np
# plot some random data
np.random.seed(2021)
df = pd.DataFrame({'y': np.random.normal(10, 100, 50).cumsum() + 2000},
index=np.arange(101, 151))
ax = df.plot(figsize=(12, 5))
# find an "interesting" point
max_ind = df['y'].argmax()
max_x = df.index[max_ind]
max_y = df.iloc[max_ind]['y']
# calculate the aspect ratio
xscale, yscale = ax.transData.transform([1, 1]) - ax.transData.transform([0, 0])
# draw the ellipse to be displayed as circle
radius_x = 4
radius_y = radius_x * xscale / yscale
ax.add_patch(Ellipse((max_x, max_y), radius_x, radius_y, color='purple', alpha=0.4))
# use ax.scatter to draw a red dot at the top left
ax.scatter(0.05, 0.9, marker='o', s=2000, color='red', transform=ax.transAxes)
plt.show()
Some remarks about drawing the ellipse:
this will only work for linear coordinates, not e.g. for logscale or polar coordinates
the code supposes nor the axis limits nor the axis position will change afterwards, as these will distort the aspect ratio
The issue seems to be that your X (passed to xy=) is not always the same as your Y, thus the oval instead of a perfect circle.
I need to put a transparent boxplot on another image with grid. I create an image with a size based on data I pass. Once the boxplot is drawn, the initial size of the image has changed. Here my part of code with further explications:
png_width = 280
png_height = 120
px = 1 / plt.rcParams['figure.dpi'] # pixel in inches
fig_width = math.ceil((png_width / 60) * (max(data) - min(data)))
fig_cnt += 1
fig = plt.figure(fig_cnt, figsize=(fig_width * px, png_height * px), frameon=False, clear=True)
ax = fig.add_subplot(111)
bp = ax.boxplot(data, vert=False, widths=0.2, whis=[0, 100], )
for median in bp['medians']:
median.set(color='#000000')
plt.axis('off')
plt.savefig(filepath, bbox_inches='tight', pad_inches=0, transparent=True)
plt.close()
For a given set of data I have min=3 and max=60. fig_width=266px which is correct. Now I want to draw a boxplot where the distance from the first whisker to the second is also 266px. In my example the saved image has only 206px. I do not need any axis on the boxplot, only the boxplot itself. Why isn't the initial image size not maintained?
There is a default margin around the subplots, so set the axes to the whole fig area by
fig.subplots_adjust(0, 0, 1, 1)
and your saved image is exactly 266 px wide.
The distance between the whiskers' ends will be smaller due to autoscaling. If you want the whiskers to use up the entire x-axis you can set the x limits accordingly:
ax.set_xlim(bp['whiskers'][0].get_xdata()[1], bp['whiskers'][1].get_xdata()[1])
(you may want to increase the limits a bit to see the full line width of the caps)
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:
The code looks perfectly fine. You can see this if you plot just 4 points (n=2):
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()
... 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...
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()
As soon as you add an edge (so a non-zero border around the markers), they will 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')
I have two lists, one of them is names, the other one is values. I want y axis to be values, x axis to be names. But names are too long to be put on axis, that's why I want to put them into bars, like on the picture, but bars should be vertical.
On the picture, my namelist represent names of cities.
My input is as such:
mylist=[289.657,461.509,456.257]
nameslist=['Bacillus subtilis','Caenorhabditis elegans','Arabidopsis thaliana']
my code:
fig = plt.figure()
width = 0.35
ax = fig.add_axes([1,1,1,1])
ax.bar(nameslist,mylist,width)
ax.set_ylabel('Average protein length')
ax.set_xlabel('Names')
ax.set_title('Average protein length by bacteria')
Any help appreciated!
ax.text can be used to place text at a given x and y position. To fit into a vertical bar, the text should be rotated 90 degrees. The text could either start at the top, or have its anchor point at the bottom. The alignment should be respectively top or bottom. A fontsize can be chosen to fit well to the image. The text color should be sufficiently contrasting to the color of the bars. An additional space can be used to have some padding.
Alternatively, there is ax.annotate with more options for positioning and decorating.
from matplotlib import pyplot as plt
import numpy as np
mylist = [289.657, 461.509, 456.257]
nameslist = ['Bacillus subtilis', 'Caenorhabditis elegans', 'Arabidopsis thaliana']
fig, ax = plt.subplots()
width = 0.35
ax.bar(nameslist, mylist, width, color='darkorchid')
for i, (name, height) in enumerate(zip(nameslist, mylist)):
ax.text(i, height, ' ' + name, color='seashell',
ha='center', va='top', rotation=-90, fontsize=18)
ax.set_ylabel('Average protein length')
ax.set_title('Average protein length by bacteria')
ax.set_xticks([]) # remove the xticks, as the labels are now inside the bars
plt.show()
I'm attempting to create a divisionary curve on a scatter plot in matplotlib that would divide my scatterplot according to marker size.
The (x,y) are phi0 and phi0dot and I'm coloring/sizing according a to third variable 'e-folds'. I'd like to draw an 'S' shaped curve that divides the plot into small, black markers and large, cyan markers.
Here is a sample scatterplot run with a very few number of points for an example. Ultimately I will run with tens of thousands of points of data such that the divisionary would be much finer and more obviously 'S' shaped. This is roughly what I have in mind.
My code thus far looks like this:
# Set up the PDF
pdf_pages = PdfPages(outfile)
plt.rcParams["font.family"] = "serif"
# Create the canvas
canvas = plt.figure(figsize=(14.0, 14.0), dpi=100)
plt.subplot(1, 1, 1)
for a, phi0, phi0dot, efolds in datastore:
if efolds[-1] > 65:
plt.scatter(phi0[0], phi0dot[0], s=200, color='aqua')
else:
plt.scatter(phi0[0], phi0dot[0], s=30, color='black')
# Apply labels
plt.xlabel(r"$\phi_0$")
plt.ylabel(r"$\dot{\phi}_0$")
# Finish the file
pdf_pages.savefig(canvas)
pdf_pages.close()
print("Finished!")
This type of separation is very akin to what I'd like to do, but don't see immediately how I would extend this to my problem. Any advice would be much appreciated.
I would assume that the separation line between the differently classified points is a simple contour line along the threshold value.
Here I'm assuming classification takes values of 0 or 1, hence one can draw a contour along 0.5,
ax.contour(x,y,clas, [0.5])
Example:
import numpy as np
import matplotlib.pyplot as plt
# Some data on a grid
x,y = np.meshgrid(np.arange(20), np.arange(10))
z = np.sin(y+1) + 2*np.cos(x/5) + 2
fig, ax = plt.subplots()
# Threshold; values above the threshold belong to another class as those below.
thresh = 2.5
clas = z > thresh
size = 100*clas + 30*~clas
# scatter plot
ax.scatter(x.flatten(), y.flatten(), s = size.flatten(), c=clas.flatten(), cmap="bwr")
# threshold line
ax.contour(x,y,clas, [.5], colors="k", linewidths=2)
plt.show()