Python matplotlib中的曲线文本渲染

Python matplotlib中的曲线文本渲染,python,matplotlib,Python,Matplotlib,在我正在做的项目中,我必须从结构化文件(xml)中接收用户输入。该文件包含一个区域的道路数据,我必须将其绘制到matplotlib画布上。问题是,除了道路,我还必须渲染道路名称,并且大多数道路都是弯曲的。我知道如何以一定角度渲染文本。但是我想知道是否有可能在字符串中间改变文本角度 大概是这样的: 但是使用matplotlib 我发现您的问题非常有趣,因此我使用matplotlib文本工具制作了一些非常接近的内容: from __future__ import division import it

在我正在做的项目中,我必须从结构化文件(xml)中接收用户输入。该文件包含一个区域的道路数据,我必须将其绘制到matplotlib画布上。问题是,除了道路,我还必须渲染道路名称,并且大多数道路都是弯曲的。我知道如何以一定角度渲染文本。但是我想知道是否有可能在字符串中间改变文本角度

大概是这样的:


但是使用matplotlib

我发现您的问题非常有趣,因此我使用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))

实施还远远不够完美,但我认为这是一个很好的起点

此外,matplotlib中似乎有一些关于标记旋转的散点图的发展,这对于这种情况是理想的。然而,我的编程技能几乎没有解决这个问题所需要的那么核心,所以我在这里无能为力


以下是我对这个问题的看法: 为了使文本对绘图后的图形调整具有鲁棒性,我从
matplotlib.text
派生了一个子类
CurvedText
CurvedText
对象以
x
-和
y
-值数组的形式获取字符串和曲线。要显示的文本本身被切割成单独的字符,每个字符都添加到绘图的适当位置。由于
matplotlib.text
在字符串为空时不绘制任何内容,因此我将所有空格替换为不可见的“a”。调整
figure
后,重载的
draw()
调用
update\u positions()
函数,该函数负责确保字符位置和方向保持正确。为了确保调用顺序(也将调用每个字符的
draw()
函数),
CurvedText
对象还注意每个字符的
zorder
高于其自身的
zorder
。按照我的示例,文本可以有任何对齐方式。如果文本无法以当前分辨率适应曲线,则其余文本将隐藏,但在调整大小时显示。下面是应用程序示例的代码

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()
从matplotlib导入pyplot作为plt
从matplotlib导入修补程序
从matplotlib将文本导入为多行文字
将numpy作为np导入
输入数学
类CurvedText(mtext.Text):
"""
沿着任意曲线的文本对象。
"""
定义初始值(自、x、y、文本、轴、**kwargs):
超级(曲线文本,自我)。\uuuuu初始(x[0],y[0],'',**kwargs)
轴。添加艺术家(自我)
##保存曲线:
自
自我。_y=y
self.self\u zorder=self.get\u zorder()
##创建文本对象
self.\uu字符=[]
对于文本中的c:
如果c='':
##让这成为一个无形的“a”:
t=mtext.Text(0,0,'a')
t、 设定α(0.0)
其他:
t=mtext.Text(0,0,c,**kwargs)
#重置不必要的参数
t、 集_ha('中心')
t、 设置旋转(0)
t、 设置顺序(self.\u顺序+1)
self.u字符.追加((c,t))
轴。添加艺术家(t)
##重载某些成员函数,以确保功能正确
##更新
def set_zorder(自我,zorder):
超级(曲线文本,自我)。设置左序(左序)
self.self\u zorder=self.get\u zorder()
对于c,t,在self.\u字符中:
t、 设置顺序(self.\u顺序+1)
def绘制(自绘制、渲染器、*args、**kwargs):
"""
Text.draw()函数的重载。请勿
执行任何绘图,但更新位置和旋转
自我的角度。
"""
自我更新位置(渲染器)
def更新_位置(自身、渲染器):
"""
更新单个文本元素的位置和旋转。
"""
#准备工作
##确定纵横比:
##从https://stackoverflow.com/a/42014041/2454357
##数据限制
xlim=self.axes.get_xlim()
ylim=self.axes.get_ylim()
##图形上的轴尺寸
figW,figH=self.axes.get_figure().get_size_inches()
##显示单元的比率
_,u,w,h=self.axes.get_position().bounds
##最终纵横比
纵横比=((figW*w)/(figH*h))*(ylim[1]-ylim[0])/(xlim[1]-xlim[0])
#图形坐标中的曲线点:
x_图,y_图=(
np.数组(l)表示zip中的l(*self.axes.transData.transform([
(i,j)表示拉链中的i,j(self.\uux,self.\uy)
]))
)
#图形坐标中的点距离
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)
#图形坐标中的弧长
l\u fig=np.插入(np.累计(r\u fig\u dist),0,0)
#图形坐标中的角度
rads=np.arctan2((y_图[1:]-y_图[:-1]),(x_图[1:]-x_图[:-1]))
degs=np.rad2deg(rads)
相对位置=10
对于c,t,在self.\u字符中:
#查找c的宽度:
t、 设置旋转(0)
t、 集合(“中心”)
bbox1=t.get\u window\u区段(渲染器=渲染器)
w=bbox1.5cm宽
h=bbox1.5米高度
#忽略所有不合适的字母:
如果rel_pos+w/2>l_图[-1]:
t、 设定α(0.0)
相对位置+=w
持续
elif c!='':
t、 设定α(1.0)
#查找两个数据点之间的水平
#角色的中心点将位于
#左右索引:
il=np.其中(相对位置+w/2>=l\U图)[0][1]

ir=np。你在哪里解决过这个问题?我自己需要。谢谢。@tommy.carstensen-I