【问题标题】:Imitating the waterfall plots in Origin with Matplotlib使用 Matplotlib 模仿 Origin 中的瀑布图
【发布时间】:2019-04-21 08:24:04
【问题描述】:

我正在尝试使用 Python 和 Matplotlib 创建由 Origin 制作的瀑布图(见下图)。

或

一般方案对我来说很有意义,您从 2D 矩阵开始,就好像您想制作曲面图,然后您可以按照the StackOverflow question here 中显示的任何方法进行操作。这个想法是将矩阵的每一行绘制为 3D 空间中的单独曲线。

这个 matplotlib 方法会产生如下图:

我遇到的困难是,在 Origin 图中清晰的透视感在 matplotlib 版本中丢失了。您可以争辩说这部分是由于相机角度,但我认为更重要的是它来自更近的线出现在更远的线的“前面”。

我的问题是,你将如何正确地在 Matplotlib 中用透视效果模仿 Origin 中的瀑布图?我真的不明白这两个情节如此不同的地方是什么,所以即使定义确切的问题也很困难。

【问题讨论】:

  • 见plot_wireframe 和plot_surface。还有unchained我对matplotlib一无所知。
  • 在我看来,原点图更像filled polygon 图表。
  • 默认为perspective rendering。所以这似乎不是问题。
  • 您是否尝试过使用上面建议的填充多边形?
  • 嘿 KF Gauss,我已经完全重写了下面的答案,希望您可以使用其中的某些部分来获得您想要的东西。请注意,您可能正在寻找的主要“技巧”是zorder 和使用多边形来强调“前面”/“后面”的组合。

标签: python matplotlib graph


【解决方案1】:

更新:您现在已经更新了您的问题以更清楚地说明您的目标,让我演示三种不同的方法来绘制此类数据,它们都有很多优点和缺点。 一般的要点(至少对我而言!)是 matplotlib 在 3D 中糟糕,尤其是在创建可发布的人物时( 我个人认为,您的里程可能会有所不同。)

我做了什么:我使用了您发布的第二张图片背后的原始数据。在所有情况下,我都使用了zorder 并添加了多边形数据(在 2D 中:fill_between(),在 3D 中:PolyCollection)来增强“3D 效果”,即启用“在彼此前面绘图”。下面的代码显示:

  • plot_2D_a() 使用颜色来表示角度,因此保持原来的y轴;虽然这在技术上现在只能用来读出最前面的线图,但它仍然给读者一种对 y 刻度的“感觉”。

  • plot_2D_b() 删除不必要的刺/刻度,而是将角度添加为文本标签;这与您发布的第二张图片最接近

  • plot_3D() 使用mplot3d 制作“3D”图;虽然现在可以旋转它来分析数据,但在尝试缩放时它会中断(至少对我而言),产生截止数据和/或隐藏轴。

在matplotlib 中,最终有许多方法 来实现瀑布图,你必须自己决定你要做什么。就个人而言,我可能大部分时间都是plot_2D_a(),因为它允许在或多或少“所有 3 个维度”中轻松重新缩放,同时还保持适当的轴(+colorbar)允许读者获取所有相关信息,一旦您将其发布在某处作为静态图像。


代码:

import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.collections import PolyCollection
import numpy as np


def offset(myFig,myAx,n=1,yOff=60):
    dx, dy = 0., yOff/myFig.dpi 
    return myAx.transData + mpl.transforms.ScaledTranslation(dx,n*dy,myFig.dpi_scale_trans)

## taken from 
## http://www.gnuplotting.org/data/head_related_impulse_responses.txt
df=pd.read_csv('head_related_impulse_responses.txt',delimiter="\t",skiprows=range(2),header=None)
df=df.transpose()

def plot_2D_a():
    """ a 2D plot which uses color to indicate the angle"""
    fig,ax=plt.subplots(figsize=(5,6))
    sampling=2
    thetas=range(0,360)[::sampling]

    cmap = mpl.cm.get_cmap('viridis')
    norm = mpl.colors.Normalize(vmin=0,vmax=360)

    for idx,i in enumerate(thetas):
        z_ind=360-idx ## to ensure each plot is "behind" the previous plot
        trans=offset(fig,ax,idx,yOff=sampling)

        xs=df.loc[0]
        ys=df.loc[i+1]

        ## note that I am using both .plot() and .fill_between(.. edgecolor="None" ..) 
        #  in order to circumvent showing the "edges" of the fill_between 
        ax.plot(xs,ys,color=cmap(norm(i)),linewidth=1, transform=trans,zorder=z_ind)
        ## try alpha=0.05 below for some "light shading"
        ax.fill_between(xs,ys,-0.5,facecolor="w",alpha=1, edgecolor="None",transform=trans,zorder=z_ind)

    cbax = fig.add_axes([0.9, 0.15, 0.02, 0.7]) # x-position, y-position, x-width, y-height
    cb1 = mpl.colorbar.ColorbarBase(cbax, cmap=cmap, norm=norm, orientation='vertical')
    cb1.set_label('Angle')

    ## use some sensible viewing limits
    ax.set_xlim(-0.2,2.2)
    ax.set_ylim(-0.5,5)

    ax.set_xlabel('time [ms]')

def plot_2D_b():
    """ a 2D plot which removes the y-axis and replaces it with text labels to indicate angles """
    fig,ax=plt.subplots(figsize=(5,6))
    sampling=2
    thetas=range(0,360)[::sampling]

    for idx,i in enumerate(thetas):
        z_ind=360-idx ## to ensure each plot is "behind" the previous plot
        trans=offset(fig,ax,idx,yOff=sampling)

        xs=df.loc[0]
        ys=df.loc[i+1]

        ## note that I am using both .plot() and .fill_between(.. edgecolor="None" ..) 
        #  in order to circumvent showing the "edges" of the fill_between 
        ax.plot(xs,ys,color="k",linewidth=0.5, transform=trans,zorder=z_ind)
        ax.fill_between(xs,ys,-0.5,facecolor="w", edgecolor="None",transform=trans,zorder=z_ind)

        ## for every 10th line plot, add a text denoting the angle. 
        #  There is probably a better way to do this.
        if idx%10==0:
            textTrans=mpl.transforms.blended_transform_factory(ax.transAxes, trans)
            ax.text(-0.05,0,u'{0}º'.format(i),ha="center",va="center",transform=textTrans,clip_on=False)

    ## use some sensible viewing limits
    ax.set_xlim(df.loc[0].min(),df.loc[0].max())
    ax.set_ylim(-0.5,5)

    ## turn off the spines
    for side in ["top","right","left"]:
        ax.spines[side].set_visible(False)
    ## and turn off the y axis
    ax.set_yticks([])

    ax.set_xlabel('time [ms]')

#--------------------------------------------------------------------------------
def plot_3D():
    """ a 3D plot of the data, with differently scaled axes"""
    fig=plt.figure(figsize=(5,6))
    ax= fig.gca(projection='3d')

    """                                                                                                                                                    
    adjust the axes3d scaling, taken from https://stackoverflow.com/a/30419243/565489
    """
    # OUR ONE LINER ADDED HERE:                to scale the    x, y, z   axes
    ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([1, 2, 1, 1]))

    sampling=2
    thetas=range(0,360)[::sampling]
    verts = []
    count = len(thetas)

    for idx,i in enumerate(thetas):
        z_ind=360-idx

        xs=df.loc[0].values
        ys=df.loc[i+1].values

        ## To have the polygons stretch to the bottom, 
        #  you either have to change the outermost ydata here, 
        #  or append one "x" pixel on each side and then run this.
        ys[0] = -0.5 
        ys[-1]= -0.5

        verts.append(list(zip(xs, ys)))        

    zs=thetas

    poly = PolyCollection(verts, facecolors = "w", edgecolors="k",linewidth=0.5 )
    ax.add_collection3d(poly, zs=zs, zdir='y')

    ax.set_ylim(0,360)
    ax.set_xlim(df.loc[0].min(),df.loc[0].max())
    ax.set_zlim(-0.5,1)

    ax.set_xlabel('time [ms]')

# plot_2D_a()
# plot_2D_b()
plot_3D()
plt.show()

【讨论】:

  • 这太棒了,正是我想要的!我看到 zorder、颜色和填充多边形的某种组合赋予了 Origin 图 3D 的感觉。
【解决方案2】:

实际上,我前段时间在为我正在写的论文创建情节时遇到了这个问题。我基本上得出了与 Asmus 相同的答案,因此我将不再详细介绍如何实现它,因为这已经涵盖了,但是我添加了具有高度相关颜色映射而不是角度相关颜色映射的功能.下面的例子:

这可能是您想要添加的内容,也可能不是,但它有助于了解数据的实际 y 值,在创建这样的瀑布图时混合 y 轴和 z 轴时会丢失该值。

这是我用来生成它的代码:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection
from matplotlib.colors import ListedColormap, BoundaryNorm

# generate data: sine wave (x-y) with 1/z frequency dependency

Nx = 200
Nz = 91
x = np.linspace(-10, 10, Nx)
z = 0.1*np.linspace(-10, 10, Nz)**2 + 4

w = 2*np.pi # omega

y = np.zeros((Nx, Nz))
for i in range(Nz):
    y[:, i] = np.cos(w*x/z[i]**0.5)/z[i]**0.2

# create waterfall plot
fig = plt.figure()
ax = fig.add_subplot(111)
for side in ['right', 'top', 'left']:
    ax.spines[side].set_visible(False)

# some usefull parameters
highest = np.max(y)
lowest = np.min(y)
delta = highest-lowest
t = np.sqrt(abs(delta))/10 # a tuning parameter for the offset of each dataset

for i in np.flip(range(Nz)):
    yi_ = y[:,i]       # the y data set
    yi = yi_ + i*t   # the shifted y data set used for plotting
    zindex = Nz-i # used to set zorder

    # fill with white from the (shifted) y data down to the lowest value
    # for good results, don't make the alpha too low, otherwise you'll get confusing blending of lines
    ax.fill_between(x, lowest, yi, facecolor="white", alpha=0.5, zorder=zindex)

    # cut the data into segments that can be colored individually
    points = np.array([x, yi]).T.reshape(-1, 1, 2)
    segments = np.concatenate([points[:-1], points[1:]], axis=1)

    # Create a continuous norm to map from data points to colors
    norm = plt.Normalize(lowest, highest)
    lc = LineCollection(segments, cmap='plasma', norm=norm)
    
    # Set the values used for colormapping
    lc.set_array(yi_)
    lc.set_zorder(zindex)
    lc.set_linewidth(1)
    line = ax.add_collection(lc)
    
    # print text indicating angle
    delta_x = max(x)-min(x)
    if (i)%10==0:
        ax.text(min(x)-5e-2*delta_x, t*i, "$\\theta=%i^\\circ$"%i, horizontalAlignment="right")

# set limits, as using LineCollection does not automatically set these
ax.set_ylim(lowest, highest + Nz*t)
ax.set_xlim(-10, 10)
fig.colorbar(line, ax=ax)
plt.yticks([])
ax.yaxis.set_ticks_position('none')
fig.savefig("waterfall_plot_cmap")

我从官方 matplotlib 示例here 中找到了如何从本教程中获取高度映射

如果有人有兴趣,我也上传了生成黑白版本的代码到我的github

【讨论】:

  • 看起来真不错:)
猜你喜欢
  • 2018-03-04
  • 2020-01-14
  • 2017-11-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多