Adjust margin around matplotlib figure with wrapped xticks or yticks labels - python

I would like to create a figure in matplotlib that contains some rather long tick-labels. To fit the labels in reasonable space, I've tried using the wrapping provided in matplotlib:
plt.yticks([1],[a],wrap=True)
This works(output), however the tick-label text then extends to the edge of the figure. How can a margin be placed between the figure edge and the tick-label text while the text is set to wrap?
Failed Attempt:
I tried to create the desired margin by expanding the bounding box(output):
f.savefig("stillnomargin.pdf",bbox_inches=f.bbox_inches.expanded(2,2))
However, the wrapping changed to redistribute the text to the new figure edge.
Full Example:
import matplotlib.pyplot as plt
labeltext = "This is a really long string that I'd rather have wrapped so that it"\
" doesn't go outside of the figure, but if it's long enough it will go"\
" off the top or bottom!"
a4_width = 8.27
a4_height = 11.69
f = plt.figure()
plt.ylim([0,2])
plt.xlim([0,2])
plt.yticks([1],[labeltext],wrap=True)
f.set_size_inches(a4_width, a4_height)
plt.subplots_adjust(left=0.3) #leave space on left for large yticklabel
# outputs full page, yticklabel text to edge
f.savefig("nomargin.pdf")
# tried to expand bounding box to create margin,
# but yticklabel then rewraps to fills space to edge
f.savefig("stillnomargin.pdf",bbox_inches=f.bbox_inches.expanded(2,2))

Related

Vertical grid lines stop appearing at certain point in large image

I am using matplotlib to draw vertical and horizontal grid lines over an image. The resulting image will have numbered squares over it. To do this, I used this code:
def drawGridOverImage(image):
"""Draw a numbered grid with 1cm2 boxes over an image."""
import matplotlib.pyplot as plt
import matplotlib.ticker as plticker
my_dpi=300.
# Set up figure
fig=plt.figure(figsize=(float(image.size[0])/my_dpi,float(image.size[1])/my_dpi),dpi=my_dpi)
ax=fig.add_subplot(111)
# Remove whitespace from around the image
fig.subplots_adjust(left=0,right=1,bottom=0,top=1)
# Set the gridding interval
# my_dpi is dots per inch, so each square is 1 inch2
myInterval=my_dpi
loc = plticker.MultipleLocator(base=myInterval)
ax.xaxis.set_major_locator(loc)
ax.yaxis.set_major_locator(loc)
# Add the grid
ax.grid(True, which='major', axis='both', linestyle='-')
# Add the image
ax.imshow(image)
# Find number of gridsquares in x and y direction
nx=abs(int(float(ax.get_xlim()[1]-ax.get_xlim()[0])/float(myInterval)))
ny=abs(int(float(ax.get_ylim()[1]-ax.get_ylim()[0])/float(myInterval)))
# Add some labels to the gridsquares
for j in range(ny):
y=myInterval/2+j*myInterval
for i in range(nx):
x=myInterval/2.+float(i)*myInterval
ax.text(x,y,'{:d}'.format(i+j*nx),color='r',ha='center',va='center')
# Save the figure
#fig.savefig('myImageGrid.tiff',dpi=my_dpi)
newImageName = nameImageWithGrid(image.filename)
fig.savefig(newImageName,dpi=my_dpi)
return fig
However, when I run this code, the vertical grid lines stop appearing at a certain point. I have attached a screenshot of part of this image (the whole image is very large) to demonstrate the problem.
After some searching, I did find this previous issue so I am not sure if this is the same issue. It is worth noting that I am working with very large images, the one in the picture above has dimensions of 11648 × 8736.
I would appreciate some help on resolving this problem and making vertical grid lines appear on whole image.
EDIT:
I tried the code on another large image that can be found in this link here and had the same problem, here is a screenshot of part of the image where the vertical grid lines stop:
I believe that the locator is the cause of the issues that are occurring now. The grid lines are drawn for the ticks created by the interval values. For example, if you change the tick marks on the x-axis like this, the grid lines are drawn correctly.
# Set the gridding interval
# my_dpi is dots per inch, so each square is 1 inch2
myInterval=my_dpi
# loc = plticker.MultipleLocator(base=myInterval)
# ax.xaxis.set_major_locator(loc)
# ax.yaxis.set_major_locator(loc)
ax.set_xticks(np.arange(0,9900,myInterval))

How can I position text relative to axes in altair?

I'm trying to make a graph much like the Multi-Line Tooltip Example. However I want to make two modifications.
First of all, I want to be able to zoom in to a portion of the data. I fixed that using alt.selection(type="interval", encodings=["x"], bind="scales") and a transform_filter. So far no problem.
The problem is that the text labels near the points are overlapping because the lines are close together. Therefore I would like to move the labels to a fixed position within the axes along the top. Is it possible to put the labels at a fixed position within the axes, even when zooming in on the graph (see mockups below)? The problem is that when you zoom in both the x and the y domains change, so the positions of the labels should be expressed as a fraction of the domains.
Another solution I could accept is when the selected value is appended to the legend labels, or some other label outside the plot area.
Mock up of the full view:
Mock up of the zoomed view:
You can control the text position with the x and y encodings. Here is an example of placing text at the top of the axis:
import altair as alt
import pandas as pd
import numpy as np
data = pd.DataFrame({
'x': np.arange(1, 21),
'y': np.random.randint(0, 20, 20),
})
points = alt.Chart(data).mark_point().encode(
x='x',
y='y'
)
text = points.mark_text(baseline='top').encode(
y=alt.value(0), # pixels from top
text='y'
)
points + text

Matplotlib: Center text in its bbox

I have to plot some data and some vertical lines to delimit interesting intervals and then I would like to add some labels to that using text. I can not entirely avoid the labels overlapping with the data or the vertical lines, so I decided to put a bbox around the text to keep it readable. My problem is that I am not able to align it centrally within this box and this is clearly visible and quite annoying in my opinion.
I'm doing something like this:
import numpy
import matplotlib
import matplotlib.pyplot as plt
fig=plt.figure()
plot=fig.add_subplot(111)
x=numpy.linspace(1,10,50)
y=numpy.random.random(50)
plot.plot(x,y)
plot.text(4.5,.5,'TEST TEST',\
bbox={'facecolor':'white','alpha':1,'edgecolor':'none','pad':1})
plot.axvline(5,color='k',linestyle='solid')
plt.show()
Which creates the following plot:
It is quite apparent, that the text is not centered in its bbox. How can I change this? I've spent quite some time on Google but I could not find anything.
EDIT:
Thanks for the suggestions so far.
This suggests that what I see is actually desired behavior. Apparently the bbox in new versions of matplotlib is chosen taking into account the possible maximum descent of the text it contains (the descent of 'g').
When a 'g' appears in the text, this does indeed look good:
Unfortunately in my case there is no 'g' or anything with a similar descent. Does anyone have any further ideas?
Use the text properties ha and va:
plot.text(5.5,.5,'TEST TEST TEST TEST',
bbox={'facecolor':'white','alpha':1,'edgecolor':'none','pad':1},
ha='center', va='center')
To check, draw lines in the center of your plot:
plot.axvline(5.5,color='k',linestyle='solid')
plot.axhline(0.5,color='k',linestyle='solid')
It seems that there are now options to properly position the text in the coordinate system (in particular the new va = 'baseline'). However, as pointed out by user35915, this does not change the alignment of the box relative to the text. The misalignment is particularly obvious in single digit numbers, in particular number '1' (see also this bug). Until this is fixed, my workaround is to place the rectangle by hand, not via the bbox parameter:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# define the rectangle size and the offset correction
rect_w = 0.2
rect_h = 0.2
rect_x_offset = 0.004
rect_y_offset =0.006
# text coordinates and content
x_text = 0.5
y_text = 0.5
text = '1'
# create the canvas
fig,ax = plt.subplots(figsize=(1,1),dpi=120)
ax.set_xlim((0,1))
ax.set_ylim((0,1))
# place the text
ax.text(x_text, y_text, text, ha="center", va="center", zorder=10)
# compare: vertical alignment with bbox-command: box is too low.
ax.text(x_text+0.3, y_text, text, ha="center", va="center",
bbox=dict(facecolor='wheat',boxstyle='square',edgecolor='black',pad=0.1), zorder=10)
# compare: horizontal alignment with bbox-command: box is too much to the left.
ax.text(x_text, y_text+0.3, text, ha="center", va="center",
bbox=dict(facecolor='wheat',boxstyle='square',edgecolor='black',pad=0.2), zorder=10)
# create the rectangle (below the text, hence the smaller zorder)
rect = patches.Rectangle((x_text-rect_w/2+rect_x_offset, y_text-rect_h/2+rect_y_offset),
rect_w,rect_h,linewidth=1,edgecolor='black',facecolor='white',zorder=9)
# add rectangle to plot
ax.add_patch(rect)
# show figure
fig.show()

matplotlib text only in plot area

I use the matplotlib library for plotting data in python. In my figure I also have some text to distinguish the data. The problem is that the text goes over the border in the figure window. Is it possible to make the border of the plot cut off the text at the corresponding position and only when I pan inside the plot the the rest of the text gets visible (but only when inside plot area). I use the text() function to display the text
[EDIT:]
The code looks like this:
fig = plt.figure()
ax = fig.add_subplot(111)
# ...
txt = ax.text(x, y, n, fontsize=10)
txt.set_clip_on(False) # I added this due to the answer from tcaswell
I think that your text goes over the border because you didn't set the limits of your plot.
Why don't you try this?
fig=figure()
ax=fig.add_subplot(1,1,1)
text(0.1, 0.85,'dummy text',horizontalalignment='left',verticalalignment='center',transform = ax.transAxes)
This way your text will always be inside the plot and its left corner will be at point (0.1,0.85) in units of your plot.
You just need to tell the text artists to not clip:
txt = ax.text(...)
txt.set_clip_on(False) # this will turn clipping off (always visible)
# txt.set_clip_on(True) # this will turn clipping on (only visible when text in data range)
However, there is a bug matplotlib (https://github.com/matplotlib/matplotlib/pull/1885 now fixed) which makes this not work. The other way to do this (as mentioned in the comments) is
to use
txt = ax.text(..., clip_on=True)

matplotlib linewidth when saving a PDF

I have a figure with some fairly delicate features that are sensitive to linewidth. I want to save this figure as a PDF that can be easily printed (i.e. no scaling on the receiver's side, just Command+P and go). Unfortunately, when I set figsize=(8.5,11) in order to correctly size the PDF for printing, matplotlib picks a very thick default linewidth and text size that mess up the plot (the legend is too large and the lines in the bar chart overlap). If I set figsize=(17,22) I get a very workable default linewidth and textsize after scaling the PDF to 50% for printing. This is what I have been using, but that solution has become unworkable due to politics and I really don't want to scale the PDF in illustrator every time I make a change.
If I could work with bitmaps I could achieve the desired result by setting figsize=(17,22) and setting dpi to half of the target dpi, but this does not work for PDFs since the dpi parameter seems to be ignored. I would like a PDF that
looks like boxes_good.png (the size-tricked bitmap with thin lines, small text)
has dimensions 8.5x11in (or prints like it does)
can be edited in illustrator (is not a bitmap wrapped in a pdf)
I can't help but suspect that there is an easy way to pull the "double size, half dpi" trick when saving as a PDF, but I gave up on getting that to work and started trying to directly manipulate the linewidths and textsizes. I succeeded in modifying textsize but not linewidth. Here is a record of the things I tried:
# Tried:
# fig.set_size_inches(17,22)
# fig.savefig('boxes.pdf', dpi=100,150,300)
# dpi parameter has no effect on linewidth, text size, or the PDF's dimensions
# 'markerscale=.5' on plt.legend and pax.legend
# no effect
# mp.rcParams['font.size']=8
# worked! made text smaller, now for linewidth...
# mp.rcParams['lines.linewidth']=5
# no effect
# fig.set_linewidth(5)
# no effect
# pax.axhline(linewidth=5)
# only changes x axis not box surrounding subplot
# fig.set_size_inches(8.5,11) immediately before plt.savefig('boxes.pdf')
# identical results to calling plt.figure(figsize=(8.5,11)) in the first place
# I tried passing plt.figure(figsize=(17,22)) and swapping it to 8.5x11 using
# fig.set_size_inches right before saving, but the lines were thick and the text
# was large in the PDF, exactly as if I had set figsize=(8.5,11) to begin with
Here is the sourcefile (I have reduced the plot to essentials, so obvious styling workarounds probably aren't workable solutions)
import numpy as np
import matplotlib as mp
import matplotlib.pyplot as plt
x = np.arange(200)
bottom_red_bar = -np.random.random(200)
bottom_black_bar = np.random.random(200) * bottom_red_bar
fig = plt.figure()
for subplotnum in [1,2,3]:
pax = plt.subplot(310+subplotnum)
pax.set_ylim([-1,1])
bot_rb = pax.bar(x, bottom_red_bar,1,color='r')
bot_bb = pax.bar(x+(1-.3)/2,bottom_black_bar,.3,color='k')
pax.legend([bot_rb,bot_bb],['Filler Text 1','Filler Text 2'],loc=4)
fig.set_size_inches(8.5,11)
fig.savefig('boxes_bad.png',dpi=300) # Lines are too thick
fig.set_size_inches(17,22)
fig.savefig('boxes_good.png',dpi=150) # Lines are just right
fig.set_size_inches(8.5,11)
plt.savefig('boxes.pdf') # Lines are too thick
fig.set_size_inches(17,22) # Lines are just right
plt.savefig('boxes.pdf') # but the PDF needs scaling before printing
So I'm after a way to either adjust the linewidth of an entire figure or a way to have matplotlib save a pdf with dimension metadata different from figsize. Any suggestions?
Thanks Marius, I'll upvote as soon as I get 15 reputation required to do so. While your rcParams didn't quite match what I wanted to do, rcParams itself was the correct place to look so I listed rcParams containing 'linewidth' via rcParams.keys():
>>> [s for s in mp.rcParams.keys() if 'linewidth' in s]
['axes.linewidth', 'grid.linewidth', 'lines.linewidth', 'patch.linewidth']
After some experimentation, I matched up what each param controlled:
mp.rcParams['axes.linewidth']: the square surrounding the entire plot (not ticks, not the y=0 line)
mp.rcParams['grid.linewidth']: didn't test, presumably grid width
mp.rcParams['lines.linewidth']: the width of line plots made using pyplot.plot
mp.rcParams['patch.linewidth']: the width of rectangle strokes including the bars of a pyplot.bar plot, legends, and legend labels of bar plots
mp.rcParams['xtick.minor.width']: the linewidth of small xticks (yticks similar)
mp.rcParams['xtick.major.width']: the linewidth of large xticks (yticks similar)
The specific solution I wound up using was
mp.rcParams['axes.linewidth'] = .5
mp.rcParams['lines.linewidth'] = .5
mp.rcParams['patch.linewidth'] = .5
I would suggest to adjust parameters like linewidth via the rcParams (or your matplotlibrc file):
# mp.rcParams['figure.figsize'] = fig_size # set figure size
mp.rcParams['font.size'] = font_size
mp.rcParams['axes.labelsize'] = font_size
mp.rcParams['axes.linewidth'] = font_size / 12.
mp.rcParams['axes.titlesize'] = font_size
mp.rcParams['legend.fontsize'] = font_size
mp.rcParams['xtick.labelsize'] = font_size
mp.rcParams['ytick.labelsize'] = font_size
I normally use the standard figure.figsize which is (8,6) and a linewidth in the axes object that is 1/12 of the font size (eg. font_size = 16 when I include the plots in twocolumn papers).
Remember that vector graphics doesn't mean, that all lines and letters always have the same size when scaling. It means that you can scale without loosing quality or sharpness (roughly speaking).

Categories

Resources