Draw text¶
Matplotlib’s TextPath turns a text string into a path - a series of line and curve segments: a representation of the text as graphics components. That path can then be manipulated and scaled, like any other shape - so we can use it to draw text along a streamline.
# Functions for manipulating Matplotlib textPaths
import numpy as np
from matplotlib.textpath import TextPath
from matplotlib.transforms import Affine2D
from scipy.interpolate import splprep, splev
from copy import deepcopy
# Convert a string to a TextPath
# They don't really have the same units - choose the size appropriately
# or use the transformation factors sx, sy
# sy is -ve by default as I like to have coordinates running bottom to top of the plot
def string_to_path(txt, fsize=4, fprop="Serif", sx=1, sy=-1):
opath = TextPath(
(0, 0), # Arbitrary origin - we're not going to plot it - just get its size
txt,
size=fsize,
prop=fprop,
)
otransform = Affine2D().scale(sx=sx, sy=sy)
opath = otransform.transform_path(opath)
return opath
# Find the length of a curve
def curve_length(x, y):
dx = np.diff(x)
dy = np.diff(y)
distances = np.sqrt(dx**2 + dy**2)
return np.sum(distances)
# Find the length of a path - assumes the path is straight
def path_length(path):
Vx = path.vertices[:, 0]
return np.max(Vx) - np.min(Vx)
# Trim a curve to a given length
def trim_curve(x, y, length):
dx = np.diff(x)
dy = np.diff(y)
distances = np.sqrt(dx**2 + dy**2)
distances = np.cumsum(distances)
idx = np.argmax(distances > length)
return x[:idx], y[:idx]
# Map a textPath onto a curve [x,y]
# path is the textPath to be mapped
# x,y are the curve to map onto - numpy arrays
def map_path(path, x, y):
Vx, Vy = path.vertices[:, 0], path.vertices[:, 1]
dVx = (Vx - np.min(Vx)) / (np.max(Vx) - np.min(Vx)) # map onto 0-1
dVy = Vy - np.mean(Vy) # Assume original path is // to x-axis
# We want to map Vx into distance along the curve
dx = np.diff(x)
dy = np.diff(y)
distancesL = np.sqrt(dx**2 + dy**2) # Local distance
distances = np.cumsum(distancesL) # Integrated distance to point
# Trim the curve to the length of the path
distances = distances[distances < (np.max(Vx) - np.min(Vx))]
x = x[: len(distances)]
y = y[: len(distances)]
distancesL = distancesL[: len(distances)]
# Create a mapping between a uniform 0-1 range and the distance along the curve
# interpolator = interp1d(np.linspace(0, 1, len(distances)), distances)
# Map the dVx to the distance along the curve
# dVxdist = interpolator(dVx)
# Create a mapping between the distance along the curve and the curve
try:
tckC, u = splprep([y, x], s=0)
except Exception as e:
print(x, y)
raise (e)
# Map the distance along the curve to the curve
nVy, nVx = splev(dVx, tckC)
# Get the gradients at the same points
dy, dx = splev(dVx, tckC, der=1)
# Generate the mapped-path vertices
tan_g = np.zeros_like(dx)
tan_g[dx != 0] = dy[dx != 0] / dx[dx != 0]
tan_g[dx == 0] = 1.0e6
# tan_g = dy / dx
dVy[dx < 0] = -dVy[dx < 0]
ndVx = -dVy * tan_g / np.sqrt(1 + tan_g**2)
ndVy = dVy / np.sqrt(1 + tan_g**2)
nVx = nVx + ndVx
nVy = nVy + ndVy
# Make the new path with these vertices
new_path = deepcopy(path)
new_path.vertices[:, 0] = nVx
new_path.vertices[:, 1] = nVy
return new_path
# Render a single text string along a curve
# ax is the matplotlib axis to render to
# txt is the text string to render
# x,y are the curve to render along - numpy arrays, in ax data units
# colour is the colour of the text
# collision_cube is a cube of points to avoid rendering collisions
# Returns a TextPath object
def string_to_patch(
ax, txt, x, y, colour="black", fsize=4, fprop="Serif", collision_cube=None
):
opath = TextPath(
(x[0], y[0]),
txt,
size=fsize,
prop=fprop,
)
otransform = Affine2D().scale(sx=1, sy=1) # y runs bottom to top
opath = otransform.transform_path(opath)
# Map the textPath onto the curve
npath = map_path(opath, x, y)
return npath