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