使用 Matplotlib 的任意非线性颜色条

Arbirtrary non-linear colorbar using Matplotlib

我想使用 Networkx 和 Matplotlib 为网络的边缘着色,其中每个边缘 (i,j) 被赋予一个介于 0 和 1 之间的值 G[i][j]['label']

然而,这些值通常要么非常接近于 0,要么非常接近 1。然后很难将颜色的变化可视化,因为所有东西要么非常红,要么非常蓝(使用 coolwarm 颜色图)。

然后,我的想法是应用过滤器 filtR,例如以下过滤器之一:

它只是一个多项式函数,提供从 [0,1] 到 [0,1] 的双射,并在 0 或 1 附近扩展更多值。如果需要,逆函数很容易处理。

现在,我只是将它应用到边缘的值,以定义它的颜色:

cm        = plt.get_cmap('coolwarm') 
cNorm     = colors.Normalize(vmin=0., vmax=1.)
scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=cm)
colorList = []

# The color is defined by filtR(G[i][j]['label'])
val_map   = {(i,j): filtR(G[i][j]['label']) for (i,j) in G.edges()}
values    = [scalarMap.to_rgba(val_map[e]) for e in G.edges()]
edges     = nx.draw_networkx_edges(G,edge_color=values,edge_cmap=plt.get_cmap('coolwarm'))


# Definition of the colorbar :-(
sm = cmx.ScalarMappable(cmap=cmx.coolwarm)
sm.set_array(values)
plt.colorbar(sm)

现在的问题是: 我想定义对应的colorbar

现在,它显示了 filtR 函数对我的边缘的评估,这是没有意义的:过滤器的唯一目的是修改 [0,1] 区间上的颜色重新分配为了提高图表的可读性。

例如,我得到:

我对左边部分很满意,但对右边部分不满意,颜色栏应该是这样的:

这里的filter函数显然不是最好的,但是应该给大家一个更好的说明。

我试图在定义颜色条之前重新定义 values :

# Definition of the colorbar :-(

new_val_map = {(i,j): filtR(G[i][j]['label']) for (i,j) in G.edges()}
new_values  = [scalarMap.to_rgba(val_map[e]) for e in G.edges()]

sm = cmx.ScalarMappable(cmap=cmx.coolwarm)
sm.set_array(new_values)
plt.colorbar(sm)

但没有任何变化。

我对 Matplotlib 的理解有点有限,所提供的代码已经是堆栈溢出答案的拼凑而成。

您必须定义您的 own custom colormap 并在自定义 cbar 中使用它:

import matplotlib.pylab as plt
from matplotlib import colorbar, colors

def make_colormap(seq, name='mycmap'):
    """Return a LinearSegmentedColormap
    seq: a sequence of floats and RGB-tuples. The floats should be increasing
    and in the interval (0,1).
    """
    seq = [(None,) * 3, 0.0] + list(seq) + [1.0, (None,) * 3]
    cdict = {'red': [], 'green': [], 'blue': []}
    for i, item in enumerate(seq):
        if isinstance(item, float):
            r1, g1, b1 = seq[i - 1]
            r2, g2, b2 = seq[i + 1]
            cdict['red'].append([item, r1, r2])
            cdict['green'].append([item, g1, g2])
            cdict['blue'].append([item, b1, b2])
    return colors.LinearSegmentedColormap(name, cdict)

def generate_cmap(lowColor, highColor, lowBorder, highBorder):
    """Apply edge colors till borders and middle is in grey color"""
    c = colors.ColorConverter().to_rgb
    return make_colormap([c(lowColor), c('grey'),l owBorder, c('grey'), .5, \
     c('grey'), highBorder ,c('grey'), c(highColor)])

fig = plt.figure()
ax = fig.add_axes([.05, .05, .02, .7]) # position of colorbar

cbar = colorbar.ColorbarBase(ax, cmap=generate_cmap('b','r',.15,.85),
  norm=colors.Normalize(vmin=.0, vmax=1)) # set min, max of colorbar
ticks = [0.,.1,.2,.3,.4,.5,.6,.7,.8,.9,1.]
cbar.set_ticks(ticks) # add ticks
plt.show()

你有你最喜欢的颜色图(比如 coolwarm),你想根据 filtR 函数扭曲它:

Nb :此函数与最初问题中建议的函数相反。

感谢Serenity的指点:颜色图定义的工作还需完成:

def distortColorMap(cm,inv = lambda x:x):
    """Inspired from 'make_colormap' in Serenity's answer.

    Inputs : a pre-existing colormap cm, 
             the distorsion function inv

    Output : the distorted colormap"""

    def f(color,inv):
        """In the sequence definition, modifies the position of stops tup[0] according the transformation function.

           Returns the distorted sequence."""
        return map(lambda tup:(inv(tup[0]),tup[1],tup[2]),color)

    # Extract sequences from cm, apply inv
    C = cm.__dict__['_segmentdata']
    cdict = {'red': f(C['red']  ,inv), 'green': f(C['green'],inv), 'blue': f(C['blue'] ,inv)}

    name = 'new_'+cm.__dict__['name']
    return colors.LinearSegmentedColormap(name, cdict)

那么,这就非常好用了:

cm        = plt.get_cmap('coolwarm')
cm        = distortColorMap(cm,inv = filtR) # all the job is done here

cNorm     = colors.Normalize(vmin=0., vmax=1.)
scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=cm)

# The color is the natural value G[i][j]['label']
val_map   = {(i,j): G[i][j]['label'] for (i,j) in G.edges()}
values    = [scalarMap.to_rgba(val_map[e]) for e in G.edges()]
edges     = nx.draw_networkx_edges(G,edge_color=values,edge_cmap=plt.get_cmap('coolwarm'))


# Definition of the colorbar : just use the new colormap
sm = cmx.ScalarMappable(cmap=cm)
sm.set_array(values)
plt.colorbar(sm)

然后我们得到相应的颜色条:

这很酷,因为您不再需要定义整个颜色序列(现在一切都从失真函数的定义中完成),并且因为您仍然可以使用 Matplotlib 提供的精美颜色图!

编辑

有关 filtR 功能和我的动机的更多信息。

在此示例中,filtR 定义为:

exponent = 7. 
filtR    = lambda y: ((2*y-1)**(1./exponent)+1.)/2.

对于 exponent 的不同值,我们有一个 class 函数(具有或多或少的平滑行为)。能够从一种定义跳到另一种定义有助于确定最佳可视化效果。

实际上,对于任何e(奇偶数),Python不喜欢在x为负数时处理x**1/e。但这没什么大不了的,我们只需正确定义 7 根(或任何其他奇数指数)即可。

然而,这不是热点:我们只需要从 [0,1] 到 [0,1] 的数学双射。然后我们就可以选择最符合我们需要的那个。

例如,我们也可以将 filtR 函数定义为 filtR = lambda y: y**4,因为我们希望在最低值上有更好的可读性。我们会得到:

它也应该适用于对数、分段或阶梯函数...

我想要一个通用且灵活的工具,它可以让我快速专注于某些特定领域。我不想为每个可视化测试手动创建带有停止点和颜色值的序列。

如果需要,我还希望能够将这项工作重新用于其他项目。

基本上您根本不想更改颜色图。 Instaed 你想创建你的自定义规范化。为此,您可以子类化 matplotlib.colors.Normalize 并让它 return 自定义函数的值。该函数需要将 vminvmax 之间的值作为输入,并将 return 范围内的值作为输入 [0,1].

import matplotlib.pyplot as plt
import numpy as np
import matplotlib.colors as mcolors


class MyNormalize(mcolors.Normalize):
    def __call__(self, value, clip=None):
        # function to normalize any input between vmin and vmax linearly to [0,1]
        n = lambda x: (x-self.vmin)/(self.vmax-self.vmin)
        # nonlinear function between [0,1] and [0,1]
        f = lambda x,a: (2*x)**a*(2*x<1)/2. +(2-(2*(1-1*x))**a)*(2*x>=1)/2.
        return np.ma.masked_array(f(n(value),0.5))


fig, (ax,ax2) = plt.subplots(ncols=2)

x = np.linspace(-0.3,1.2, num=101)
X = (np.sort(np.random.rand(100))*1.5-0.3)

norm=  MyNormalize(vmin=-0.3, vmax=1.2)

ax.plot(x,norm(x))
im = ax2.imshow(X[::-1,np.newaxis], norm=norm, cmap="coolwarm", aspect="auto")
fig.colorbar(im)

plt.show()

所需颜色条的图像更像是一个部分线性函数,就像下面使用的蜜蜂一样。

class MyNormalize2(mcolors.Normalize):
    def __call__(self, value, clip=None):
        n = lambda x: self.vmin+(self.vmax-self.vmin)*x
        x, y = [self.vmin, n(0.2), n(0.8), self.vmax], [0, 0.48,0.52, 1]
        return np.ma.masked_array(np.interp(value, x, y))