Representación de texto curvo en matplotlib

En un proyecto que estoy haciendo, tengo que tomar una entrada de usuario de un archivo estructurado (xml). El archivo contiene datos de carreteras de un área, que tengo que trazar en el canvas de matplotlib. El problema es que junto con la carretera, también tengo que representar el nombre de la carretera, y la mayoría de las carreteras son curvas. Sé cómo representar el texto en un ángulo. Pero me preguntaba si es posible cambiar el ángulo del texto a mitad de la cadena.

Algo como esto: dibujar texto girado en camino curvo

Pero utilizando matplotlib.

Aquí está mi opinión sobre el problema: para hacer que el texto sea robusto a los ajustes de figura después del dibujo, CurvedText una clase secundaria, CurvedText , de matplotlib.text . El objeto CurvedText toma una cadena y una curva en forma de matrices de CurvedText x e y . El texto que se va a mostrar se corta en caracteres separados, cada uno de los cuales se agrega a la gráfica en la posición apropiada. Como matplotlib.text no dibuja nada si la cadena está vacía, sustituyo todos los espacios por invisibles. Tras el ajuste de la figure , el draw() sobrecargado llama a la función update_positions() , que se encarga de que las posiciones y orientaciones de los caracteres se mantengan correctas. Para garantizar el orden de llamada (también se llamará la función draw() cada carácter), el objeto CurvedText también se encarga de que el zorder de cada carácter sea más alto que su propio zorder . Siguiendo mi ejemplo aquí , el texto puede tener cualquier alineación. Si el texto no se puede ajustar a la curva en la resolución actual, el rest se ocultará, pero aparecerá al cambiar el tamaño. A continuación se muestra el código con un ejemplo de aplicación.

 from matplotlib import pyplot as plt from matplotlib import patches from matplotlib import text as mtext import numpy as np import math class CurvedText(mtext.Text): """ A text object that follows an arbitrary curve. """ def __init__(self, x, y, text, axes, **kwargs): super(CurvedText, self).__init__(x[0],y[0],' ', **kwargs) axes.add_artist(self) ##saving the curve: self.__x = x self.__y = y self.__zorder = self.get_zorder() ##creating the text objects self.__Characters = [] for c in text: if c == ' ': ##make this an invisible 'a': t = mtext.Text(0,0,'a') t.set_alpha(0.0) else: t = mtext.Text(0,0,c, **kwargs) #resetting unnecessary arguments t.set_ha('center') t.set_rotation(0) t.set_zorder(self.__zorder +1) self.__Characters.append((c,t)) axes.add_artist(t) ##overloading some member functions, to assure correct functionality ##on update def set_zorder(self, zorder): super(CurvedText, self).set_zorder(zorder) self.__zorder = self.get_zorder() for c,t in self.__Characters: t.set_zorder(self.__zorder+1) def draw(self, renderer, *args, **kwargs): """ Overload of the Text.draw() function. Do not do do any drawing, but update the positions and rotation angles of self.__Characters. """ self.update_positions(renderer) def update_positions(self,renderer): """ Update positions and rotations of the individual text elements. """ #preparations ##determining the aspect ratio: ##from https://stackoverflow.com/a/42014041/2454357 ##data limits xlim = self.axes.get_xlim() ylim = self.axes.get_ylim() ## Axis size on figure figW, figH = self.axes.get_figure().get_size_inches() ## Ratio of display units _, _, w, h = self.axes.get_position().bounds ##final aspect ratio aspect = ((figW * w)/(figH * h))*(ylim[1]-ylim[0])/(xlim[1]-xlim[0]) #points of the curve in figure coordinates: x_fig,y_fig = ( np.array(l) for l in zip(*self.axes.transData.transform([ (i,j) for i,j in zip(self.__x,self.__y) ])) ) #point distances in figure coordinates x_fig_dist = (x_fig[1:]-x_fig[:-1]) y_fig_dist = (y_fig[1:]-y_fig[:-1]) r_fig_dist = np.sqrt(x_fig_dist**2+y_fig_dist**2) #arc length in figure coordinates l_fig = np.insert(np.cumsum(r_fig_dist),0,0) #angles in figure coordinates rads = np.arctan2((y_fig[1:] - y_fig[:-1]),(x_fig[1:] - x_fig[:-1])) degs = np.rad2deg(rads) rel_pos = 10 for c,t in self.__Characters: #finding the width of c: t.set_rotation(0) t.set_va('center') bbox1 = t.get_window_extent(renderer=renderer) w = bbox1.width h = bbox1.height #ignore all letters that don't fit: if rel_pos+w/2 > l_fig[-1]: t.set_alpha(0.0) rel_pos += w continue elif c != ' ': t.set_alpha(1.0) #finding the two data points between which the horizontal #center point of the character will be situated #left and right indices: il = np.where(rel_pos+w/2 >= l_fig)[0][-1] ir = np.where(rel_pos+w/2 <= l_fig)[0][0] #if we exactly hit a data point: if ir == il: ir += 1 #how much of the letter width was needed to find il: used = l_fig[il]-rel_pos rel_pos = l_fig[il] #relative distance between il and ir where the center #of the character will be fraction = (w/2-used)/r_fig_dist[il] ##setting the character position in data coordinates: ##interpolate between the two points: x = self.__x[il]+fraction*(self.__x[ir]-self.__x[il]) y = self.__y[il]+fraction*(self.__y[ir]-self.__y[il]) #getting the offset when setting correct vertical alignment #in data coordinates t.set_va(self.get_va()) bbox2 = t.get_window_extent(renderer=renderer) bbox1d = self.axes.transData.inverted().transform(bbox1) bbox2d = self.axes.transData.inverted().transform(bbox2) dr = np.array(bbox2d[0]-bbox1d[0]) #the rotation/stretch matrix rad = rads[il] rot_mat = np.array([ [math.cos(rad), math.sin(rad)*aspect], [-math.sin(rad)/aspect, math.cos(rad)] ]) ##computing the offset vector of the rotated character drp = np.dot(dr,rot_mat) #setting final position and rotation: t.set_position(np.array([x,y])+drp) t.set_rotation(degs[il]) t.set_va('center') t.set_ha('center') #updating rel_pos to right edge of character rel_pos += w-used if __name__ == '__main__': Figure, Axes = plt.subplots(2,2, figsize=(7,7), dpi=100) N = 100 curves = [ [ np.linspace(0,1,N), np.linspace(0,1,N), ], [ np.linspace(0,2*np.pi,N), np.sin(np.linspace(0,2*np.pi,N)), ], [ -np.cos(np.linspace(0,2*np.pi,N)), np.sin(np.linspace(0,2*np.pi,N)), ], [ np.cos(np.linspace(0,2*np.pi,N)), np.sin(np.linspace(0,2*np.pi,N)), ], ] texts = [ 'straight lines work the same as rotated text', 'wavy curves work well on the convex side', 'you even can annotate parametric curves', 'changing the plotting direction also changes text orientation', ] for ax, curve, text in zip(Axes.reshape(-1), curves, texts): #plotting the curve ax.plot(*curve, color='b') #adjusting plot limits stretch = 0.2 xlim = ax.get_xlim() w = xlim[1] - xlim[0] ax.set_xlim([xlim[0]-stretch*w, xlim[1]+stretch*w]) ylim = ax.get_ylim() h = ylim[1] - ylim[0] ax.set_ylim([ylim[0]-stretch*h, ylim[1]+stretch*h]) #adding the text text = CurvedText( x = curve[0], y = curve[1], text=text,#'this this is a very, very long text', va = 'bottom', axes = ax, ##calls ax.add_artist in __init__ ) plt.show() 

El resultado se ve así:

texto curvo en matplotlib

Todavía hay algunos problemas, cuando el texto sigue el lado cóncavo de una curva fuertemente curvada. Esto se debe a que los caracteres se "unen" a lo largo de la curva sin tener en cuenta la superposición. Si tengo tiempo, trataré de mejorar eso. Cualquier comentario es muy bienvenido.

Probado en python 3.5 y 2.7

Tu problema me pareció bastante interesante, así que hice algo que se acerca bastante con la herramienta de texto matplotlib:

 from __future__ import division import itertools import matplotlib.pyplot as plt import numpy as np %matplotlib inline # define figure and axes properties fig, ax = plt.subplots(figsize=(8,6)) ax.set_xlim(left=0, right=10) ax.set_ylim(bottom=-1.5, top=1.5) (xmin, xmax), (ymin, ymax) = ax.get_xlim(), ax.get_ylim() # calculate a shape factor, more explanation on usage further # it is a representation of the distortion of the actual image compared to a # cartesian space: fshape = abs(fig.get_figwidth()*(xmax - xmin)/(ymax - ymin)/fig.get_figheight()) # the text you want to plot along your line thetext = 'the text is flowing ' # generate a cycler, so that the string is cycled through lettercycler = itertools.cycle(tuple(thetext)) # generate dummy river coordinates xvals = np.linspace(1, 10, 300) yvals = np.sin(xvals)**3 # every XX datapoints, a character is printed markerevery = 10 # calculate the rotation angle for the labels (in degrees) # the angle is calculated as the slope between two datapoints. # it is then multiplied by a shape factor to get from the angles in a # cartesian space to the angles in this figure # first calculate the slope between two consecutive points, multiply with the # shape factor, get the angle in radians with the arctangens functions, and # convert to degrees angles = np.rad2deg(np.arctan((yvals[1:]-yvals[:-1])/(xvals[1:]-xvals[:-1])*fshape)) # plot the 'river' ax.plot(xvals, yvals, 'b', linewidth=3) # loop over the data points, but only plot a character every XX steps for counter in np.arange(0, len(xvals)-1, step=markerevery): # plot the character in between two datapoints xcoord = (xvals[counter] + xvals[counter+1])/2. ycoord = (yvals[counter] + yvals[counter+1])/2. # plot using the text method, set the rotation so it follows the line, # aling in the center for a nicer look, optionally, a box can be drawn # around the letter ax.text(xcoord, ycoord, lettercycler.next(), fontsize=25, rotation=angles[counter], horizontalalignment='center', verticalalignment='center', bbox=dict(facecolor='white', edgecolor='white', alpha=0.5)) 

salida de ejemplo

La implementación está lejos de ser perfecta, pero en mi opinión es un buen punto de partida.

Además, parece que hay cierto desarrollo en matplotlib al tener un diagtwig de dispersión con rotación de los marcadores, lo que sería ideal para este caso. Sin embargo, mis habilidades de progtwigción no son tan difíciles como deben ser para abordar este problema, por lo que no puedo ayudar aquí.

matplotlib en github: solicitud de extracción

matplotlib en github: problema