Reputation: 125
I want to create a sequence logo using matplotlib for my work.
Sequence logo is like this as shown in https://en.wikipedia.org/wiki/Sequence_logo. Each character has a specific height. And I'd like to make this using matplotlib.
How do I change the aspect ratio of a font? I've tried to use Affine2D from matplotlib.transform as following, but it didn't work.
ax.text(1,1,"A").set_transform(matplotlib.transforms.Affine2D().scale(0.5,1))
Is there an easy workaround?
Upvotes: 4
Views: 1159
Reputation: 16737
I've tried different ways of stretching text's width in Matplotlib but nothing worked out. Probably they didn't implement stretching correctly yet, I even saw in docs this notice like This feature is not implemented yet!
for one of font-stretching function.
So I decided to write my own helper functions to achieve this task. It uses drawing functions of PIL module, you have to install one time next modules python -m pip install pillow numpy matplotlib
.
Notice that my helper function text_draw_mpl(...)
accepts x, y offset and width, height all expressed in your plot units, e.g. if you have x/y ranging from 0 to 1 on your plot then you have to use in function values like 0.1, 0.2, 0.3, 0.4.
My other helper function text_draw_np(...)
is low level function, probably you'll never use it straight away, it uses width, height expressed in pixels and produces on output RGB array of shape (height, width, 3)
(3 for RGB colors).
In my functions you may pass background color (bg
argument) and foreground color (color
argument) both as string color name (like 'magenta'
or 'blue'
) and also as RGB tuple like (0, 255, 0)
for green color. By default if not provided foreground color is black and background color is white.
Notice that my function supports argument remove_gaps
, if it is True
that empty space will be removed from all sides of drawn text picture, if it is False
then empty space remains. Empty space is introduced by the way Glyphs are drawn inside Font file, e.g. small letter m
has more space at the top, capital letter T
less space at the top. Font has this space so that whole text has same height and also that two lines of text over each other have some gap between and don't merge.
Also notice that I provided path to default Windows Arial font c:/windows/fonts/arial.ttf
, if you have Linux, or you want other font, just download any free Unicode TrueType (.ttf) font from internet (e.g. from here) and put that font nearby to your script and modify path in my code below. Also PIL module supports other formats, as stated in its docs Supported: TrueType and OpenType fonts (as well as other font formats supported by the FreeType library)
.
def text_draw_np(text, width, height, *, font = 'c:/windows/fonts/arial.ttf', bg = (255, 255, 255), color = (0, 0, 0), remove_gaps = False, cache = {}):
import math, numpy as np, PIL.Image, PIL.ImageDraw, PIL.ImageFont, PIL.ImageColor
def get_font(fname, size):
key = ('font', fname, size)
if key not in cache:
cache[key] = PIL.ImageFont.truetype(fname, size = size, encoding = 'unic')
return cache[key]
width, height = math.ceil(width), math.ceil(height)
pil_font = get_font(font, 24)
text_width, text_height = pil_font.getsize(text)
pil_font = get_font(font, math.ceil(1.2 * 24 * max(width / text_width, height / text_height)))
text_width, text_height = pil_font.getsize(text)
canvas = PIL.Image.new('RGB', (text_width, text_height), bg)
draw = PIL.ImageDraw.Draw(canvas)
draw.text((0, 0), text, font = pil_font, fill = color)
if remove_gaps:
a = np.asarray(canvas)
bg_rgb = PIL.ImageColor.getrgb(bg)
b = np.zeros_like(a)
b[:, :, 0] = bg_rgb[0]; b[:, :, 1] = bg_rgb[1]; b[:, :, 2] = bg_rgb[2]
t0 = np.any((a != b).reshape(a.shape[0], -1), axis = -1)
top, bot = np.flatnonzero(t0)[0], np.flatnonzero(t0)[-1]
t0 = np.any((a != b).transpose(1, 0, 2).reshape(a.shape[1], -1), axis = -1)
lef, rig = np.flatnonzero(t0)[0], np.flatnonzero(t0)[-1]
a = a[top : bot, lef : rig]
canvas = PIL.Image.fromarray(a)
canvas = canvas.resize((width, height), PIL.Image.LANCZOS)
return np.asarray(canvas)
def text_draw_mpl(fig, ax, text, offset_x, offset_y, width, height, **nargs):
axbb = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
pxw, pxh = axbb.width * fig.dpi * width / (ax.get_xlim()[1] - ax.get_xlim()[0]), axbb.height * fig.dpi * height / (ax.get_ylim()[1] - ax.get_ylim()[0])
ax.imshow(text_draw_np(text, pxw * 1.2, pxh * 1.2, **nargs), extent = (offset_x, offset_x + width, offset_y, offset_y + height), aspect = 'auto', interpolation = 'lanczos')
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.set_ylim(0, 1000)
ax.set_xlim(0, 1000)
text_draw_mpl(fig, ax, 'Hello!', 100, 500, 150, 500, color = 'green', bg = 'magenta', remove_gaps = True)
text_draw_mpl(fig, ax, 'World!', 100, 200, 800, 100, color = 'blue', bg = 'yellow', remove_gaps = True)
text_draw_mpl(fig, ax, ' Gaps ', 400, 500, 500, 200, color = 'red', bg = 'gray', remove_gaps = False)
plt.show()
Output:
Upvotes: 2