在数据单位中扩展指定宽度的行。

13

我的问题与这个问题有点类似,它是关于在数据坐标系下给线条设置线宽的。使我的问题更具挑战性的是,与链接问题不同的是,我想要扩展的线段具有随机方向。

假设线段从(0, 10)(10, 10),我希望将其扩展到6的宽度。那么就是:

x = [0, 10]
y = [10, 10]
ax.fill_between(x, y - 3, y + 3)

然而,我的线段具有随机方向。也就是说,它不一定沿着x轴或y轴。它具有一定的斜率

线段s被定义为其起始点和结束点的列表:[(x1, y1), (x2, y2)]

现在我希望将这条线段扩展到一定的宽度w期望的解决方案适用于任何方向的线段。怎么做?

plt.plot(x, y, linewidth=6.0)不能解决问题,因为我希望我的宽度与数据的单位相同。


为什么不能使用线宽参数?plt.plot(x, y, linewidth=6.0) - beroe
1
我猜你真正想要画矩形。 - tacaswell
@tcaswell 这可能是一个不错的主意。目前我所拥有的是中心线。对于如何实现您有什么建议吗? - user2881553
1
这看起来非常类似于这个问题 https://dev59.com/cXDXa4cB1Zd3GeqP9zhL - Jakob
1
@Jakob,不完全相同,如果您的目标是保存的图形,则不需要缩放支持。mavErick,我想我已经在下面使其工作了,但如果您想要多个子图,则必须调整缩放因子。 - beroe
显示剩余3条评论
3个回答

15

以下代码是使用matplotlib绘制数据坐标为线宽的折线图的通用示例。有两种解决方案:一种使用回调,一种使用子类Line2D。

使用回调函数。

这个功能是以类data_linewidth_plot的形式实现的,可以使用与普通plt.plot命令非常接近的签名进行调用。

l = data_linewidth_plot(x, y, ax=ax, label='some line', linewidth=1, alpha=0.4)

ax参数是要绘图的轴。当图中仅存在一个子图时,可以省略ax参数。linewidth参数的解释以(y轴)数据单位为准。

进一步的功能:

  1. 不受子图位置、边距或图形大小的影响。
  2. 如果纵横比例不相等,则使用y轴数据坐标作为线宽。
  3. 还确保图例处理正确(我们可能希望在图中有一条巨大的线,但在图例中绝对不会)。
  4. 与图形大小、缩放或平移事件的更改兼容,因为它会在此类事件上调整线宽。

这是完整的代码。

import matplotlib.pyplot as plt

class data_linewidth_plot():
    def __init__(self, x, y, **kwargs):
        self.ax = kwargs.pop("ax", plt.gca())
        self.fig = self.ax.get_figure()
        self.lw_data = kwargs.pop("linewidth", 1)
        self.lw = 1
        self.fig.canvas.draw()

        self.ppd = 72./self.fig.dpi
        self.trans = self.ax.transData.transform
        self.linehandle, = self.ax.plot([],[],**kwargs)
        if "label" in kwargs: kwargs.pop("label")
        self.line, = self.ax.plot(x, y, **kwargs)
        self.line.set_color(self.linehandle.get_color())
        self._resize()
        self.cid = self.fig.canvas.mpl_connect('draw_event', self._resize)

    def _resize(self, event=None):
        lw =  ((self.trans((1, self.lw_data))-self.trans((0, 0)))*self.ppd)[1]
        if lw != self.lw:
            self.line.set_linewidth(lw)
            self.lw = lw
            self._redraw_later()

    def _redraw_later(self):
        self.timer = self.fig.canvas.new_timer(interval=10)
        self.timer.single_shot = True
        self.timer.add_callback(lambda : self.fig.canvas.draw_idle())
        self.timer.start()

fig1, ax1 = plt.subplots()
#ax.set_aspect('equal') #<-not necessary 
ax1.set_ylim(0,3)
x = [0,1,2,3]
y = [1,1,2,2]

# plot a line, with 'linewidth' in (y-)data coordinates.       
l = data_linewidth_plot(x, y, ax=ax1, label='some 1 data unit wide line', 
                        linewidth=1, alpha=0.4)

plt.legend() # <- legend possible
plt.show()

进入图像描述

(由于此问题),我更新了代码,使用定时器重新绘制画布。

Line2D子类化

上述解决方案有一些缺点。它需要一个定时器和回调来在更改轴限制或图形大小时更新自身。以下是一种没有这种需求的解决方案。它将使用动态属性从所需数据坐标中始终计算出线宽度的点数。比以上方法短得多。 这里的缺点是需要手动通过代理艺术家创建传说。

import matplotlib.pyplot as plt
from matplotlib.lines import Line2D

class LineDataUnits(Line2D):
    def __init__(self, *args, **kwargs):
        _lw_data = kwargs.pop("linewidth", 1) 
        super().__init__(*args, **kwargs)
        self._lw_data = _lw_data

    def _get_lw(self):
        if self.axes is not None:
            ppd = 72./self.axes.figure.dpi
            trans = self.axes.transData.transform
            return ((trans((1, self._lw_data))-trans((0, 0)))*ppd)[1]
        else:
            return 1

    def _set_lw(self, lw):
        self._lw_data = lw

    _linewidth = property(_get_lw, _set_lw)


fig, ax = plt.subplots()

#ax.set_aspect('equal') # <-not necessary, if not given, y data is assumed 
ax.set_xlim(0,3)
ax.set_ylim(0,3)
x = [0,1,2,3]
y = [1,1,2,2]

line = LineDataUnits(x, y, linewidth=1, alpha=0.4)
ax.add_line(line)

ax.legend([Line2D([],[], linewidth=3, alpha=0.4)], 
           ['some 1 data unit wide line'])    # <- legend possible via proxy artist
plt.show()

1
谢谢,但是...就你所知,截至2021年4月25日,获得这样一个基本目标仍然如此复杂吗? - Antonio Sesto

11

仅仅是为了补充之前的回答(还不能评论),这里提供一个函数,可以自动化这个过程,无需等轴或者启发式值0.8来标记。在调用此函数后,数据极限和轴的大小需要被固定且不能更改。

def linewidth_from_data_units(linewidth, axis, reference='y'):
    """
    Convert a linewidth in data units to linewidth in points.

    Parameters
    ----------
    linewidth: float
        Linewidth in data units of the respective reference-axis
    axis: matplotlib axis
        The axis which is used to extract the relevant transformation
        data (data limits and size must not change afterwards)
    reference: string
        The axis that is taken as a reference for the data width.
        Possible values: 'x' and 'y'. Defaults to 'y'.

    Returns
    -------
    linewidth: float
        Linewidth in points
    """
    fig = axis.get_figure()
    if reference == 'x':
        length = fig.bbox_inches.width * axis.get_position().width
        value_range = np.diff(axis.get_xlim())
    elif reference == 'y':
        length = fig.bbox_inches.height * axis.get_position().height
        value_range = np.diff(axis.get_ylim())
    # Convert length to points
    length *= 72
    # Scale linewidth to value range
    return linewidth * (length / value_range)

应该用 fig.dpi 而不是 72,对吧? - Phyks
经过进一步的测试,似乎确实是 72,而不是 fig.dpi。但我不确定原因... - Phyks
完美,除非需要标量而不是数组:np.diff(axis.get_lim()) 应该改为 np.diff(axis.get_lim())[0]。谢谢! - Walt W
3
为什么“72”是正确的数字:线宽以点为单位给出。点的单位通常是每英寸72个点,也是在matplotlib中如此。虽然每英寸的像素数可能会改变,但每英寸的点数保持不变。 - ImportanceOfBeingErnest

7

说明:

  • 设置一个已知高度的图形,并使两个轴的比例相等(否则“数据坐标”的概念不适用)。确保图形的比例与x和y轴的预期比例相匹配

  • 通过将英寸乘以72来计算整个图形的高度point_hei(包括边距)的单位

  • 手动分配y轴范围yrange(您可以通过首先绘制“虚拟”系列,然后查询绘图轴以获取下限和上限y限制来完成此操作。)

  • 提供您想要的线宽度数据单位linewid

  • 计算在调整边距的同时为单位的那些单位pointlinewid。在单帧绘图中,绘图占完整图像高度的80%。

  • 绘制线条,使用不填充线条末端的capstyle(在这些大型线条上有很大影响)

嗨,完成了吗?(注意:这应该会在保存的文件中生成正确的图像,但如果您调整绘图窗口的大小,则不保证。)

import matplotlib.pyplot as plt
rez=600
wid=8.0 # Must be proportional to x and y limits below
hei=6.0
fig = plt.figure(1, figsize=(wid, hei))
sp = fig.add_subplot(111)
# # plt.figure.tight_layout() 
# fig.set_autoscaley_on(False)
sp.set_xlim([0,4000])
sp.set_ylim([0,3000])
plt.axes().set_aspect('equal')

# line is in points: 72 points per inch
point_hei=hei*72 

xval=[100,1300,2200,3000,3900]
yval=[10,200,2500,1750,1750]
x1,x2,y1,y2 = plt.axis()
yrange =   y2 - y1
# print yrange

linewid = 500     # in data units

# For the calculation below, you have to adjust width by 0.8
# because the top and bottom 10% of the figure are labels & axis
pointlinewid = (linewid * (point_hei/yrange)) * 0.8  # corresponding width in pts

plt.plot(xval,yval,linewidth = pointlinewid,color="blue",solid_capstyle="butt")
# just for fun, plot the half-width line on top of it
plt.plot(xval,yval,linewidth = pointlinewid/2,color="red",solid_capstyle="butt")

plt.savefig('mymatplot2.png',dpi=rez)

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接