在 matplotlib 中定义颜色图的中点

发布于 2024-12-04 12:01:19 字数 94 浏览 0 评论 0 原文

我想设置颜色图的中间点,即我的数据从-5到10,我希望零作为中间点。我认为做到这一点的方法是通过子类化标准化并使用规范,但我没有找到任何示例,而且我不清楚,我到底要实现什么?

I want to set the middle point of a colormap, i.e., my data goes from -5 to 10 and I want zero to be the middle point. I think the way to do it is by subclassing normalize and using the norm, but I didn't find any example and it is not clear to me, what exactly have I to implement?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

我还不会笑 2024-12-11 12:01:19

我知道这已经晚了,但我刚刚经历了这个过程,并提出了一个解决方案,该解决方案可能不如子类规范化强大,但简单得多。我认为在这里与后代分享是件好事。

函数

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import AxesGrid

def shiftedColorMap(cmap, start=0, midpoint=0.5, stop=1.0, name='shiftedcmap'):
    '''
    Function to offset the "center" of a colormap. Useful for
    data with a negative min and positive max and you want the
    middle of the colormap's dynamic range to be at zero.

    Input
    -----
      cmap : The matplotlib colormap to be altered
      start : Offset from lowest point in the colormap's range.
          Defaults to 0.0 (no lower offset). Should be between
          0.0 and `midpoint`.
      midpoint : The new center of the colormap. Defaults to 
          0.5 (no shift). Should be between 0.0 and 1.0. In
          general, this should be  1 - vmax / (vmax + abs(vmin))
          For example if your data range from -15.0 to +5.0 and
          you want the center of the colormap at 0.0, `midpoint`
          should be set to  1 - 5/(5 + 15)) or 0.75
      stop : Offset from highest point in the colormap's range.
          Defaults to 1.0 (no upper offset). Should be between
          `midpoint` and 1.0.
    '''
    cdict = {
        'red': [],
        'green': [],
        'blue': [],
        'alpha': []
    }

    # regular index to compute the colors
    reg_index = np.linspace(start, stop, 257)

    # shifted index to match the data
    shift_index = np.hstack([
        np.linspace(0.0, midpoint, 128, endpoint=False), 
        np.linspace(midpoint, 1.0, 129, endpoint=True)
    ])

    for ri, si in zip(reg_index, shift_index):
        r, g, b, a = cmap(ri)

        cdict['red'].append((si, r, r))
        cdict['green'].append((si, g, g))
        cdict['blue'].append((si, b, b))
        cdict['alpha'].append((si, a, a))

    newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict)
    plt.register_cmap(cmap=newcmap)

    return newcmap

示例 示例

biased_data = np.random.random_integers(low=-15, high=5, size=(37,37))

orig_cmap = matplotlib.cm.coolwarm
shifted_cmap = shiftedColorMap(orig_cmap, midpoint=0.75, name='shifted')
shrunk_cmap = shiftedColorMap(orig_cmap, start=0.15, midpoint=0.75, stop=0.85, name='shrunk')

fig = plt.figure(figsize=(6,6))
grid = AxesGrid(fig, 111, nrows_ncols=(2, 2), axes_pad=0.5,
                label_mode="1", share_all=True,
                cbar_location="right", cbar_mode="each",
                cbar_size="7%", cbar_pad="2%")

# normal cmap
im0 = grid[0].imshow(biased_data, interpolation="none", cmap=orig_cmap)
grid.cbar_axes[0].colorbar(im0)
grid[0].set_title('Default behavior (hard to see bias)', fontsize=8)

im1 = grid[1].imshow(biased_data, interpolation="none", cmap=orig_cmap, vmax=15, vmin=-15)
grid.cbar_axes[1].colorbar(im1)
grid[1].set_title('Centered zero manually,\nbut lost upper end of dynamic range', fontsize=8)

im2 = grid[2].imshow(biased_data, interpolation="none", cmap=shifted_cmap)
grid.cbar_axes[2].colorbar(im2)
grid[2].set_title('Recentered cmap with function', fontsize=8)

im3 = grid[3].imshow(biased_data, interpolation="none", cmap=shrunk_cmap)
grid.cbar_axes[3].colorbar(im3)
grid[3].set_title('Recentered cmap with function\nand shrunk range', fontsize=8)

for ax in grid:
    ax.set_yticks([])
    ax.set_xticks([])

结果:

在此处输入图像描述

I know this is late to the game, but I just went through this process and came up with a solution that perhaps less robust than subclassing normalize, but much simpler. I thought it'd be good to share it here for posterity.

The function

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import AxesGrid

def shiftedColorMap(cmap, start=0, midpoint=0.5, stop=1.0, name='shiftedcmap'):
    '''
    Function to offset the "center" of a colormap. Useful for
    data with a negative min and positive max and you want the
    middle of the colormap's dynamic range to be at zero.

    Input
    -----
      cmap : The matplotlib colormap to be altered
      start : Offset from lowest point in the colormap's range.
          Defaults to 0.0 (no lower offset). Should be between
          0.0 and `midpoint`.
      midpoint : The new center of the colormap. Defaults to 
          0.5 (no shift). Should be between 0.0 and 1.0. In
          general, this should be  1 - vmax / (vmax + abs(vmin))
          For example if your data range from -15.0 to +5.0 and
          you want the center of the colormap at 0.0, `midpoint`
          should be set to  1 - 5/(5 + 15)) or 0.75
      stop : Offset from highest point in the colormap's range.
          Defaults to 1.0 (no upper offset). Should be between
          `midpoint` and 1.0.
    '''
    cdict = {
        'red': [],
        'green': [],
        'blue': [],
        'alpha': []
    }

    # regular index to compute the colors
    reg_index = np.linspace(start, stop, 257)

    # shifted index to match the data
    shift_index = np.hstack([
        np.linspace(0.0, midpoint, 128, endpoint=False), 
        np.linspace(midpoint, 1.0, 129, endpoint=True)
    ])

    for ri, si in zip(reg_index, shift_index):
        r, g, b, a = cmap(ri)

        cdict['red'].append((si, r, r))
        cdict['green'].append((si, g, g))
        cdict['blue'].append((si, b, b))
        cdict['alpha'].append((si, a, a))

    newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict)
    plt.register_cmap(cmap=newcmap)

    return newcmap

An example

biased_data = np.random.random_integers(low=-15, high=5, size=(37,37))

orig_cmap = matplotlib.cm.coolwarm
shifted_cmap = shiftedColorMap(orig_cmap, midpoint=0.75, name='shifted')
shrunk_cmap = shiftedColorMap(orig_cmap, start=0.15, midpoint=0.75, stop=0.85, name='shrunk')

fig = plt.figure(figsize=(6,6))
grid = AxesGrid(fig, 111, nrows_ncols=(2, 2), axes_pad=0.5,
                label_mode="1", share_all=True,
                cbar_location="right", cbar_mode="each",
                cbar_size="7%", cbar_pad="2%")

# normal cmap
im0 = grid[0].imshow(biased_data, interpolation="none", cmap=orig_cmap)
grid.cbar_axes[0].colorbar(im0)
grid[0].set_title('Default behavior (hard to see bias)', fontsize=8)

im1 = grid[1].imshow(biased_data, interpolation="none", cmap=orig_cmap, vmax=15, vmin=-15)
grid.cbar_axes[1].colorbar(im1)
grid[1].set_title('Centered zero manually,\nbut lost upper end of dynamic range', fontsize=8)

im2 = grid[2].imshow(biased_data, interpolation="none", cmap=shifted_cmap)
grid.cbar_axes[2].colorbar(im2)
grid[2].set_title('Recentered cmap with function', fontsize=8)

im3 = grid[3].imshow(biased_data, interpolation="none", cmap=shrunk_cmap)
grid.cbar_axes[3].colorbar(im3)
grid[3].set_title('Recentered cmap with function\nand shrunk range', fontsize=8)

for ax in grid:
    ax.set_yticks([])
    ax.set_xticks([])

Results of the example:

enter image description here

夜访吸血鬼 2024-12-11 12:01:19

请注意,在 matplotlib 版本 3.2+ 中 TwoSlopeNorm 类。我认为它涵盖了您的用例。
它可以这样使用:

from matplotlib import colors
divnorm=colors.TwoSlopeNorm(vmin=-5., vcenter=0., vmax=10)
pcolormesh(your_data, cmap="coolwarm", norm=divnorm)

在 matplotlib 3.1 中,该类被称为 DivergingNorm

Note that in matplotlib version 3.2+ the TwoSlopeNorm class was added. I think it covers your use case.
It can be used like this:

from matplotlib import colors
divnorm=colors.TwoSlopeNorm(vmin=-5., vcenter=0., vmax=10)
pcolormesh(your_data, cmap="coolwarm", norm=divnorm)

In matplotlib 3.1 the class was called DivergingNorm.

鸩远一方 2024-12-11 12:01:19

最简单的方法是使用 imshowvminvmax 参数(假设您正在处理图像数据),而不是子类化 matplotlib .colors.Normalize

例如

import numpy as np
import matplotlib.pyplot as plt

data = np.random.random((10,10))
# Make the data range from about -5 to 10
data = 10 / 0.75 * (data - 0.25)

plt.imshow(data, vmin=-10, vmax=10)
plt.colorbar()

plt.show()

在此处输入图像描述

It's easiest to just use the vmin and vmax arguments to imshow (assuming you're working with image data) rather than subclassing matplotlib.colors.Normalize.

E.g.

import numpy as np
import matplotlib.pyplot as plt

data = np.random.random((10,10))
# Make the data range from about -5 to 10
data = 10 / 0.75 * (data - 0.25)

plt.imshow(data, vmin=-10, vmax=10)
plt.colorbar()

plt.show()

enter image description here

暗藏城府 2024-12-11 12:01:19

这是归一化归一化的解决方案。使用它的

norm = MidPointNorm(midpoint=3)
imshow(X, norm=norm)

类如下:

import numpy as np
from numpy import ma
from matplotlib import cbook
from matplotlib.colors import Normalize

class MidPointNorm(Normalize):    
    def __init__(self, midpoint=0, vmin=None, vmax=None, clip=False):
        Normalize.__init__(self,vmin, vmax, clip)
        self.midpoint = midpoint

    def __call__(self, value, clip=None):
        if clip is None:
            clip = self.clip

        result, is_scalar = self.process_value(value)

        self.autoscale_None(result)
        vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint

        if not (vmin < midpoint < vmax):
            raise ValueError("midpoint must be between maxvalue and minvalue.")       
        elif vmin == vmax:
            result.fill(0) # Or should it be all masked? Or 0.5?
        elif vmin > vmax:
            raise ValueError("maxvalue must be bigger than minvalue")
        else:
            vmin = float(vmin)
            vmax = float(vmax)
            if clip:
                mask = ma.getmask(result)
                result = ma.array(np.clip(result.filled(vmax), vmin, vmax),
                                  mask=mask)

            # ma division is very slow; we can take a shortcut
            resdat = result.data

            #First scale to -1 to 1 range, than to from 0 to 1.
            resdat -= midpoint            
            resdat[resdat>0] /= abs(vmax - midpoint)            
            resdat[resdat<0] /= abs(vmin - midpoint)

            resdat /= 2.
            resdat += 0.5
            result = ma.array(resdat, mask=result.mask, copy=False)                

        if is_scalar:
            result = result[0]            
        return result

    def inverse(self, value):
        if not self.scaled():
            raise ValueError("Not invertible until scaled")
        vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint

        if cbook.iterable(value):
            val = ma.asarray(value)
            val = 2 * (val-0.5)  
            val[val>0]  *= abs(vmax - midpoint)
            val[val<0] *= abs(vmin - midpoint)
            val += midpoint
            return val
        else:
            val = 2 * (value - 0.5)
            if val < 0: 
                return  val*abs(vmin-midpoint) + midpoint
            else:
                return  val*abs(vmax-midpoint) + midpoint

Here is a solution subclassing Normalize. To use it

norm = MidPointNorm(midpoint=3)
imshow(X, norm=norm)

Here is the Class:

import numpy as np
from numpy import ma
from matplotlib import cbook
from matplotlib.colors import Normalize

class MidPointNorm(Normalize):    
    def __init__(self, midpoint=0, vmin=None, vmax=None, clip=False):
        Normalize.__init__(self,vmin, vmax, clip)
        self.midpoint = midpoint

    def __call__(self, value, clip=None):
        if clip is None:
            clip = self.clip

        result, is_scalar = self.process_value(value)

        self.autoscale_None(result)
        vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint

        if not (vmin < midpoint < vmax):
            raise ValueError("midpoint must be between maxvalue and minvalue.")       
        elif vmin == vmax:
            result.fill(0) # Or should it be all masked? Or 0.5?
        elif vmin > vmax:
            raise ValueError("maxvalue must be bigger than minvalue")
        else:
            vmin = float(vmin)
            vmax = float(vmax)
            if clip:
                mask = ma.getmask(result)
                result = ma.array(np.clip(result.filled(vmax), vmin, vmax),
                                  mask=mask)

            # ma division is very slow; we can take a shortcut
            resdat = result.data

            #First scale to -1 to 1 range, than to from 0 to 1.
            resdat -= midpoint            
            resdat[resdat>0] /= abs(vmax - midpoint)            
            resdat[resdat<0] /= abs(vmin - midpoint)

            resdat /= 2.
            resdat += 0.5
            result = ma.array(resdat, mask=result.mask, copy=False)                

        if is_scalar:
            result = result[0]            
        return result

    def inverse(self, value):
        if not self.scaled():
            raise ValueError("Not invertible until scaled")
        vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint

        if cbook.iterable(value):
            val = ma.asarray(value)
            val = 2 * (val-0.5)  
            val[val>0]  *= abs(vmax - midpoint)
            val[val<0] *= abs(vmin - midpoint)
            val += midpoint
            return val
        else:
            val = 2 * (value - 0.5)
            if val < 0: 
                return  val*abs(vmin-midpoint) + midpoint
            else:
                return  val*abs(vmax-midpoint) + midpoint
冷情妓 2024-12-11 12:01:19

在这里,我创建了 Normalize 的子类,后面是一个最小的示例。

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt


class MidpointNormalize(mpl.colors.Normalize):
    def __init__(self, vmin, vmax, midpoint=0, clip=False):
        self.midpoint = midpoint
        mpl.colors.Normalize.__init__(self, vmin, vmax, clip)

    def __call__(self, value, clip=None):
        normalized_min = max(0, 1 / 2 * (1 - abs((self.midpoint - self.vmin) / (self.midpoint - self.vmax))))
        normalized_max = min(1, 1 / 2 * (1 + abs((self.vmax - self.midpoint) / (self.midpoint - self.vmin))))
        normalized_mid = 0.5
        x, y = [self.vmin, self.midpoint, self.vmax], [normalized_min, normalized_mid, normalized_max]
        return np.ma.masked_array(np.interp(value, x, y))


vals = np.array([[-5., 0], [5, 10]]) 
vmin = vals.min()
vmax = vals.max()

norm = MidpointNormalize(vmin=vmin, vmax=vmax, midpoint=0)
cmap = 'RdBu_r' 

plt.imshow(vals, cmap=cmap, norm=norm)
plt.colorbar()
plt.show()

结果: pic -1

仅包含正数据的同一示例 vals = np.array([[1., 3], [6, 10]])

pic-2

属性:

  • 中点获取中间颜色。
  • 上限和下限范围通过相同的线性变换重新调整。
  • 颜色栏中仅显示图片上出现的颜色。
  • 即使 vmin 大于 midpoint 似乎也能正常工作(但没有测试所有边缘情况)。

此解决方案的灵感来自此页面中的同名类

Here I create a subclass of Normalize followed by a minimal example.

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt


class MidpointNormalize(mpl.colors.Normalize):
    def __init__(self, vmin, vmax, midpoint=0, clip=False):
        self.midpoint = midpoint
        mpl.colors.Normalize.__init__(self, vmin, vmax, clip)

    def __call__(self, value, clip=None):
        normalized_min = max(0, 1 / 2 * (1 - abs((self.midpoint - self.vmin) / (self.midpoint - self.vmax))))
        normalized_max = min(1, 1 / 2 * (1 + abs((self.vmax - self.midpoint) / (self.midpoint - self.vmin))))
        normalized_mid = 0.5
        x, y = [self.vmin, self.midpoint, self.vmax], [normalized_min, normalized_mid, normalized_max]
        return np.ma.masked_array(np.interp(value, x, y))


vals = np.array([[-5., 0], [5, 10]]) 
vmin = vals.min()
vmax = vals.max()

norm = MidpointNormalize(vmin=vmin, vmax=vmax, midpoint=0)
cmap = 'RdBu_r' 

plt.imshow(vals, cmap=cmap, norm=norm)
plt.colorbar()
plt.show()

Result: pic-1

The same example with only positive data vals = np.array([[1., 3], [6, 10]])

pic-2

Properties:

  • The midpoint gets the middle color.
  • Upper and lower ranges are rescaled by the same linear transformation.
  • Only the color which appear on the picture are shown in the colorbar.
  • Seems to work fine even if vmin is bigger than midpoint (did not test all the edge cases though).

This solution is inspired by a class with the same name from this page

偷得浮生 2024-12-11 12:01:19

不确定您是否仍在寻找答案。对我来说,尝试子类化 Normalize 并不成功。因此,我专注于手动创建一个新的数据集、刻度和刻度标签,以获得我认为您想要的效果。

我在 matplotlib 中发现了 scale 模块,该模块有一个用于通过“syslog”规则转换线图的类,因此我使用它来转换数据。然后我缩放数据,使其从 0 变为 1(Normalize 通常执行的操作),但我对正数的缩放方式与负数的缩放方式不同。这是因为你的 vmax 和 vmin 可能不一样,所以 0.5 -> 1 可能覆盖比 0.5 更大的正范围 -> 0.5。 0,负数范围。对我来说,创建一个例程来计算刻度值和标签值更容易。

下面是代码和示例图。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.mpl as mpl
import matplotlib.scale as scale

NDATA = 50
VMAX=10
VMIN=-5
LINTHRESH=1e-4

def makeTickLables(vmin,vmax,linthresh):
    """
    make two lists, one for the tick positions, and one for the labels
    at those positions. The number and placement of positive labels is 
    different from the negative labels.
    """
    nvpos = int(np.log10(vmax))-int(np.log10(linthresh))
    nvneg = int(np.log10(np.abs(vmin)))-int(np.log10(linthresh))+1
    ticks = []
    labels = []
    lavmin = (np.log10(np.abs(vmin)))
    lvmax = (np.log10(np.abs(vmax)))
    llinthres = int(np.log10(linthresh))
    # f(x) = mx+b
    # f(llinthres) = .5
    # f(lavmin) = 0
    m = .5/float(llinthres-lavmin)
    b = (.5-llinthres*m-lavmin*m)/2
    for itick in range(nvneg):
        labels.append(-1*float(pow(10,itick+llinthres)))
        ticks.append((b+(itick+llinthres)*m))
    # add vmin tick
    labels.append(vmin)
    ticks.append(b+(lavmin)*m)

    # f(x) = mx+b
    # f(llinthres) = .5
    # f(lvmax) = 1
    m = .5/float(lvmax-llinthres)
    b = m*(lvmax-2*llinthres) 
    for itick in range(1,nvpos):
        labels.append(float(pow(10,itick+llinthres)))
        ticks.append((b+(itick+llinthres)*m))
    # add vmax tick
    labels.append(vmax)
    ticks.append(b+(lvmax)*m)

    return ticks,labels


data = (VMAX-VMIN)*np.random.random((NDATA,NDATA))+VMIN

# define a scaler object that can transform to 'symlog'
scaler = scale.SymmetricalLogScale.SymmetricalLogTransform(10,LINTHRESH)
datas = scaler.transform(data)

# scale datas so that 0 is at .5
# so two seperate scales, one for positive and one for negative
data2 = np.where(np.greater(data,0),
                 .75+.25*datas/np.log10(VMAX),
                 .25+.25*(datas)/np.log10(np.abs(VMIN))
                 )

ticks,labels=makeTickLables(VMIN,VMAX,LINTHRESH)

cmap = mpl.cm.jet
fig = plt.figure()
ax = fig.add_subplot(111)
im = ax.imshow(data2,cmap=cmap,vmin=0,vmax=1)
cbar = plt.colorbar(im,ticks=ticks)
cbar.ax.set_yticklabels(labels)

fig.savefig('twoscales.png')

vmax=10,vmin=-5 and linthresh=1e-4

随意调整“常量”(例如 VMAX)位于脚本顶部,以确认其运行良好。

Not sure if you are still looking for an answer. For me, trying to subclass Normalize was unsuccessful. So I focused on manually creating a new data set, ticks and tick-labels to get the effect I think you are aiming for.

I found the scale module in matplotlib that has a class used to transform line plots by the 'syslog' rules, so I use that to transform the data. Then I scale the data so that it goes from 0 to 1 (what Normalize usually does), but I scale the positive numbers differently from the negative numbers. This is because your vmax and vmin might not be the same, so .5 -> 1 might cover a larger positive range than .5 -> 0, the negative range does. It was easier for me to create a routine to calculate the tick and label values.

Below is the code and an example figure.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.mpl as mpl
import matplotlib.scale as scale

NDATA = 50
VMAX=10
VMIN=-5
LINTHRESH=1e-4

def makeTickLables(vmin,vmax,linthresh):
    """
    make two lists, one for the tick positions, and one for the labels
    at those positions. The number and placement of positive labels is 
    different from the negative labels.
    """
    nvpos = int(np.log10(vmax))-int(np.log10(linthresh))
    nvneg = int(np.log10(np.abs(vmin)))-int(np.log10(linthresh))+1
    ticks = []
    labels = []
    lavmin = (np.log10(np.abs(vmin)))
    lvmax = (np.log10(np.abs(vmax)))
    llinthres = int(np.log10(linthresh))
    # f(x) = mx+b
    # f(llinthres) = .5
    # f(lavmin) = 0
    m = .5/float(llinthres-lavmin)
    b = (.5-llinthres*m-lavmin*m)/2
    for itick in range(nvneg):
        labels.append(-1*float(pow(10,itick+llinthres)))
        ticks.append((b+(itick+llinthres)*m))
    # add vmin tick
    labels.append(vmin)
    ticks.append(b+(lavmin)*m)

    # f(x) = mx+b
    # f(llinthres) = .5
    # f(lvmax) = 1
    m = .5/float(lvmax-llinthres)
    b = m*(lvmax-2*llinthres) 
    for itick in range(1,nvpos):
        labels.append(float(pow(10,itick+llinthres)))
        ticks.append((b+(itick+llinthres)*m))
    # add vmax tick
    labels.append(vmax)
    ticks.append(b+(lvmax)*m)

    return ticks,labels


data = (VMAX-VMIN)*np.random.random((NDATA,NDATA))+VMIN

# define a scaler object that can transform to 'symlog'
scaler = scale.SymmetricalLogScale.SymmetricalLogTransform(10,LINTHRESH)
datas = scaler.transform(data)

# scale datas so that 0 is at .5
# so two seperate scales, one for positive and one for negative
data2 = np.where(np.greater(data,0),
                 .75+.25*datas/np.log10(VMAX),
                 .25+.25*(datas)/np.log10(np.abs(VMIN))
                 )

ticks,labels=makeTickLables(VMIN,VMAX,LINTHRESH)

cmap = mpl.cm.jet
fig = plt.figure()
ax = fig.add_subplot(111)
im = ax.imshow(data2,cmap=cmap,vmin=0,vmax=1)
cbar = plt.colorbar(im,ticks=ticks)
cbar.ax.set_yticklabels(labels)

fig.savefig('twoscales.png')

vmax=10,vmin=-5 and linthresh=1e-4

Feel free to adjust the "constants" (eg VMAX) at the top of the script to confirm that it behaves well.

情绪少女 2024-12-11 12:01:19

对于 matplotlib 版本 3.4 或更高版本,也许最简单的解决方案是使用新的 CenteredNorm

使用 CenteredNorm 和 发散颜色图之一的示例:

import matplotlib.pyplot as plt
import matplotlib as mpl

plt.pcolormesh(data_to_plot, norm=mpl.colors.CenteredNorm(), cmap='coolwarm')

很简单,CenteredNorm 是对称的,因此如果数据从 -5 到 10,颜色图将从 -10 拉伸到 10。
如果您希望在中心的两侧使用不同的映射,以便颜色图范围从 -5 到 10,请使用 TwoSlopeNorm 如 @macKaiver 的答案中所述。

With matplotlib version 3.4 or later, the perhaps simplest solution is to use the new CenteredNorm.

Example using CenteredNorm and one of the diverging colormaps:

import matplotlib.pyplot as plt
import matplotlib as mpl

plt.pcolormesh(data_to_plot, norm=mpl.colors.CenteredNorm(), cmap='coolwarm')

Being simple, CenteredNorm is symmetrical, so that if the data goes from -5 to 10, the colormap will be stretched from -10 to 10.
If you want a different mapping on either side of the center, so that the colormap ranges from -5 to 10, use the TwoSlopeNorm as described in @macKaiver's answer.

韵柒 2024-12-11 12:01:19

我使用了 Paul H 的出色答案,但遇到了一个问题,因为我的一些数据范围从负到正,而其他数据集范围从 0 到正或从负到 0;无论哪种情况,我都希望 0 被着色为白色(我正在使用的颜色图的中点)。在现有实现中,如果您的中点值等于 1 或 0,则原始映射不会被覆盖。您可以在下图中看到:
之前的图表编辑></a><br />
第三列看起来是正确的,但第二列中的深蓝色区域和其余列中的深红色区域都应该是白色的(它们的数据值实际上是 0)。使用我的修复程序给了我:<br />
<a href=

def shiftedColorMap(cmap, min_val, max_val, name):
    '''Function to offset the "center" of a colormap. Useful for data with a negative min and positive max and you want the middle of the colormap's dynamic range to be at zero. Adapted from https://stackoverflow.com/questions/7404116/defining-the-midpoint-of-a-colormap-in-matplotlib

    Input
    -----
      cmap : The matplotlib colormap to be altered.
      start : Offset from lowest point in the colormap's range.
          Defaults to 0.0 (no lower ofset). Should be between
          0.0 and `midpoint`.
      midpoint : The new center of the colormap. Defaults to
          0.5 (no shift). Should be between 0.0 and 1.0. In
          general, this should be  1 - vmax/(vmax + abs(vmin))
          For example if your data range from -15.0 to +5.0 and
          you want the center of the colormap at 0.0, `midpoint`
          should be set to  1 - 5/(5 + 15)) or 0.75
      stop : Offset from highets point in the colormap's range.
          Defaults to 1.0 (no upper ofset). Should be between
          `midpoint` and 1.0.'''
    epsilon = 0.001
    start, stop = 0.0, 1.0
    min_val, max_val = min(0.0, min_val), max(0.0, max_val) # Edit #2
    midpoint = 1.0 - max_val/(max_val + abs(min_val))
    cdict = {'red': [], 'green': [], 'blue': [], 'alpha': []}
    # regular index to compute the colors
    reg_index = np.linspace(start, stop, 257)
    # shifted index to match the data
    shift_index = np.hstack([np.linspace(0.0, midpoint, 128, endpoint=False), np.linspace(midpoint, 1.0, 129, endpoint=True)])
    for ri, si in zip(reg_index, shift_index):
        if abs(si - midpoint) < epsilon:
            r, g, b, a = cmap(0.5) # 0.5 = original midpoint.
        else:
            r, g, b, a = cmap(ri)
        cdict['red'].append((si, r, r))
        cdict['green'].append((si, g, g))
        cdict['blue'].append((si, b, b))
        cdict['alpha'].append((si, a, a))
    newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict)
    plt.register_cmap(cmap=newcmap)
    return newcmap

进行了编辑:编辑: 当某些我的数据范围从较小的正值到较大的正值,其中非常低的值被涂成红色而不是白色。我通过在上面的代码中添加行 Edit #2 来修复它。

I was using the excellent answer from Paul H, but ran into an issue because some of my data ranged from negative to positive, while other sets ranged from 0 to positive or from negative to 0; in either case I wanted 0 to be coloured as white (the midpoint of the colormap I'm using). With the existing implementation, if your midpoint value is equal to 1 or 0, the original mappings were not being overwritten. You can see that in the following picture:
graphs before edit
The 3rd column looks correct, but the dark blue area in the 2nd column and the dark red area in the remaining columns are all supposed to be white (their data values are in fact 0). Using my fix gives me:
graphs after edit
My function is essentially the same as that from Paul H, with my edits at the start of the for loop:

def shiftedColorMap(cmap, min_val, max_val, name):
    '''Function to offset the "center" of a colormap. Useful for data with a negative min and positive max and you want the middle of the colormap's dynamic range to be at zero. Adapted from https://stackoverflow.com/questions/7404116/defining-the-midpoint-of-a-colormap-in-matplotlib

    Input
    -----
      cmap : The matplotlib colormap to be altered.
      start : Offset from lowest point in the colormap's range.
          Defaults to 0.0 (no lower ofset). Should be between
          0.0 and `midpoint`.
      midpoint : The new center of the colormap. Defaults to
          0.5 (no shift). Should be between 0.0 and 1.0. In
          general, this should be  1 - vmax/(vmax + abs(vmin))
          For example if your data range from -15.0 to +5.0 and
          you want the center of the colormap at 0.0, `midpoint`
          should be set to  1 - 5/(5 + 15)) or 0.75
      stop : Offset from highets point in the colormap's range.
          Defaults to 1.0 (no upper ofset). Should be between
          `midpoint` and 1.0.'''
    epsilon = 0.001
    start, stop = 0.0, 1.0
    min_val, max_val = min(0.0, min_val), max(0.0, max_val) # Edit #2
    midpoint = 1.0 - max_val/(max_val + abs(min_val))
    cdict = {'red': [], 'green': [], 'blue': [], 'alpha': []}
    # regular index to compute the colors
    reg_index = np.linspace(start, stop, 257)
    # shifted index to match the data
    shift_index = np.hstack([np.linspace(0.0, midpoint, 128, endpoint=False), np.linspace(midpoint, 1.0, 129, endpoint=True)])
    for ri, si in zip(reg_index, shift_index):
        if abs(si - midpoint) < epsilon:
            r, g, b, a = cmap(0.5) # 0.5 = original midpoint.
        else:
            r, g, b, a = cmap(ri)
        cdict['red'].append((si, r, r))
        cdict['green'].append((si, g, g))
        cdict['blue'].append((si, b, b))
        cdict['alpha'].append((si, a, a))
    newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict)
    plt.register_cmap(cmap=newcmap)
    return newcmap

EDIT: I ran into a similar issue yet again when some of my data ranged from a small positive value to a larger positive value, where the very low values were being coloured red instead of white. I fixed it by adding line Edit #2 in the code above.

蘸点软妹酱 2024-12-11 12:01:19

我的解决方案涉及创建截断的颜色图,其中截断限制是根据数据中的最小值和最大值确定的。

from matplotlib import colormaps as mplcmaps

def get_centered_cmap_from_vminvmax(vmin,vmax,cmap=None):

    if cmap is None:
        cmap = mplcmaps["coolwarm"]
    
    low,high = calculate_centered_cmap_fractions(vmin,vmax,cmap.N)
    
    return get_truncated_colormap(cmap,low,high)

其中:

import numpy as np
import matplotlib.colors as mplcolors

def calculate_centered_cmap_fractions(vmin,vmax,total_idx=256):
    mid_idx = total_idx//2

    if vmin == vmax:
        raise ValueError("Cannot use the same `vmin` and `vmax`")

    if np.sign(vmin) == np.sign(vmax):
        raise ValueError("`vmin` and `vmax` need to have opposite signs, or one of them needs to be 0.")

    if vmin == 0:
        low_idx = mid_idx
        high_idx = total_idx
    elif vmax == 0:
        low_idx = 0
        high_idx = mid_idx
    elif vmin < 0 and vmax > 0:
        if abs(vmin) > vmax:
            low_idx = 0
            high_idx = mid_idx + abs(vmax/vmin)*mid_idx
        else:
            high_idx = total_idx
            low_idx = mid_idx - abs(vmin/vmax)*mid_idx
    
    return low_idx/total_idx, high_idx/total_idx

def get_truncated_colormap(cmap, minval=0.0, maxval=1.0, n=100):
    return mplcolors.LinearSegmentedColormap.from_list(
        'trunc({n},{a:.2f},{b:.2f})'.format(n=cmap.name, a=minval, b=maxval),
        cmap(np.linspace(minval, maxval, n)))

注意我的代码是专门为处理以下情况而编写的:(a) vminvmax 具有相反的符号,或 (b) 以下情况之一它们是0。如果有人愿意,可以直接将两者都为负面或正面的情况包括在内。

说明用法:

import matplotlib.pyplot as plt
import matplotlib.cm as cm

def illustrate_centered_cmap_usage(vmin,vmax,cmap = None):

    if cmap is None:
        cmap = mplcmaps["coolwarm"]
    
    fig,(ax,cax)=plt.subplots(ncols=2,gridspec_kw={"wspace":0.05,"width_ratios":[1,0.04]})

    x = np.random.normal(size=50)
    y = np.random.normal(size=50)
    c = np.random.uniform(vmin,vmax,50)

    norm = plt.Normalize(vmin=vmin, vmax=vmax)
    
    # This is the function I wrote at the very top
    cmap = get_centered_cmap_from_vminvmax(vmin,vmax,cmap=cmap)

    ax.scatter(x,y,c=cmap(norm(c)),s=200)

    plt.colorbar(cm.ScalarMappable(norm=norm,cmap=cmap),cax=cax)

    plt.show()

以情况 (a) 为例,其中 vmin < 0 和 vmax > 0:

illustrate_centered_cmap_usage(-30,100)

在此处输入图像描述

情况 (b) 的示例,其中 vmin < 0vmax == 0

illustrate_centered_cmap_usage(-30,0)

在此处输入图像描述

My solution involves creating a truncated colormap, where the truncation limits are determined from the mininimum and maximum values in your data.

from matplotlib import colormaps as mplcmaps

def get_centered_cmap_from_vminvmax(vmin,vmax,cmap=None):

    if cmap is None:
        cmap = mplcmaps["coolwarm"]
    
    low,high = calculate_centered_cmap_fractions(vmin,vmax,cmap.N)
    
    return get_truncated_colormap(cmap,low,high)

where:

import numpy as np
import matplotlib.colors as mplcolors

def calculate_centered_cmap_fractions(vmin,vmax,total_idx=256):
    mid_idx = total_idx//2

    if vmin == vmax:
        raise ValueError("Cannot use the same `vmin` and `vmax`")

    if np.sign(vmin) == np.sign(vmax):
        raise ValueError("`vmin` and `vmax` need to have opposite signs, or one of them needs to be 0.")

    if vmin == 0:
        low_idx = mid_idx
        high_idx = total_idx
    elif vmax == 0:
        low_idx = 0
        high_idx = mid_idx
    elif vmin < 0 and vmax > 0:
        if abs(vmin) > vmax:
            low_idx = 0
            high_idx = mid_idx + abs(vmax/vmin)*mid_idx
        else:
            high_idx = total_idx
            low_idx = mid_idx - abs(vmin/vmax)*mid_idx
    
    return low_idx/total_idx, high_idx/total_idx

def get_truncated_colormap(cmap, minval=0.0, maxval=1.0, n=100):
    return mplcolors.LinearSegmentedColormap.from_list(
        'trunc({n},{a:.2f},{b:.2f})'.format(n=cmap.name, a=minval, b=maxval),
        cmap(np.linspace(minval, maxval, n)))

Note my code is written specifically to handle the cases where (a) vmin and vmax have opposite signs, or (b) one of them is 0. It would be straightforward to include the cases where both are negative or positive, if one wanted.

To illustrate the usage:

import matplotlib.pyplot as plt
import matplotlib.cm as cm

def illustrate_centered_cmap_usage(vmin,vmax,cmap = None):

    if cmap is None:
        cmap = mplcmaps["coolwarm"]
    
    fig,(ax,cax)=plt.subplots(ncols=2,gridspec_kw={"wspace":0.05,"width_ratios":[1,0.04]})

    x = np.random.normal(size=50)
    y = np.random.normal(size=50)
    c = np.random.uniform(vmin,vmax,50)

    norm = plt.Normalize(vmin=vmin, vmax=vmax)
    
    # This is the function I wrote at the very top
    cmap = get_centered_cmap_from_vminvmax(vmin,vmax,cmap=cmap)

    ax.scatter(x,y,c=cmap(norm(c)),s=200)

    plt.colorbar(cm.ScalarMappable(norm=norm,cmap=cmap),cax=cax)

    plt.show()

Example of case (a), with vmin < 0 and vmax > 0:

illustrate_centered_cmap_usage(-30,100)

enter image description here

Example of case (b), with vmin < 0 and vmax == 0:

illustrate_centered_cmap_usage(-30,0)

enter image description here

予囚 2024-12-11 12:01:19

如果您不介意计算出 vmin、vmax 和零之间的比率,这是一个从蓝色到白色到红色的非常基本的线性映射,它根据比率 z 设置白色:

def colormap(z):
    """custom colourmap for map plots"""

    cdict1 = {'red': ((0.0, 0.0, 0.0),
                      (z,   1.0, 1.0),
                      (1.0, 1.0, 1.0)),
              'green': ((0.0, 0.0, 0.0),
                        (z,   1.0, 1.0),
                        (1.0, 0.0, 0.0)),
              'blue': ((0.0, 1.0, 1.0),
                       (z,   1.0, 1.0),
                       (1.0, 0.0, 0.0))
              }

    return LinearSegmentedColormap('BlueRed1', cdict1)

cdict格式相当简单:行是创建的渐变中的点:第一个条目是 x 值(沿渐变从 0 到 1 的比率),第二个条目是前一段的结束值,第三个条目是是下一段的起始值 - 如果你想要平滑梯度,后两者总是相同的。 请参阅文档了解更多详细信息。

If you don't mind working out the ratio between vmin, vmax, and zero, this is a pretty basic linear map from blue to white to red, that sets white according to the ratio z:

def colormap(z):
    """custom colourmap for map plots"""

    cdict1 = {'red': ((0.0, 0.0, 0.0),
                      (z,   1.0, 1.0),
                      (1.0, 1.0, 1.0)),
              'green': ((0.0, 0.0, 0.0),
                        (z,   1.0, 1.0),
                        (1.0, 0.0, 0.0)),
              'blue': ((0.0, 1.0, 1.0),
                       (z,   1.0, 1.0),
                       (1.0, 0.0, 0.0))
              }

    return LinearSegmentedColormap('BlueRed1', cdict1)

The cdict format is fairly simple: the rows are points in the gradient that gets created: the first entry is the x-value (the ratio along the gradient from 0 to 1), the second is the end value for the previous segment, and the third is the start value for the next segment - if you want smooth gradients, the latter two are always the same. See the docs for more detail.

妞丶爷亲个 2024-12-11 12:01:19

我遇到了类似的问题,但我希望最高值是全红色,并切断蓝色的低值,使其看起来基本上就像颜色条的底部被切掉一样。这对我有用(包括可选的透明度):

def shift_zero_bwr_colormap(z: float, transparent: bool = True):
    """shifted bwr colormap"""
    if (z < 0) or (z > 1):
        raise ValueError('z must be between 0 and 1')

    cdict1 = {'red': ((0.0, max(-2*z+1, 0), max(-2*z+1, 0)),
                      (z,   1.0, 1.0),
                      (1.0, 1.0, 1.0)),

              'green': ((0.0, max(-2*z+1, 0), max(-2*z+1, 0)),
                        (z,   1.0, 1.0),
                        (1.0, max(2*z-1,0),  max(2*z-1,0))),

              'blue': ((0.0, 1.0, 1.0),
                       (z,   1.0, 1.0),
                       (1.0, max(2*z-1,0), max(2*z-1,0))),
              }
    if transparent:
        cdict1['alpha'] = ((0.0, 1-max(-2*z+1, 0), 1-max(-2*z+1, 0)),
                           (z,   0.0, 0.0),
                           (1.0, 1-max(2*z-1,0),  1-max(2*z-1,0)))

    return LinearSegmentedColormap('shifted_rwb', cdict1)

cmap =  shift_zero_bwr_colormap(.3)

x = np.arange(0, np.pi, 0.1)
y = np.arange(0, 2*np.pi, 0.1)
X, Y = np.meshgrid(x, y)
Z = np.cos(X) * np.sin(Y) * 5 + 5
plt.plot([0, 10*np.pi], [0, 20*np.pi], color='c', lw=20, zorder=-3)
plt.imshow(Z, interpolation='nearest', origin='lower', cmap=cmap)
plt.colorbar()

I had a similar problem, but I wanted the highest value to be full red and cut off low values of blue, making it look essentially like the bottom of the colorbar was chopped off. This worked for me (includes optional transparency):

def shift_zero_bwr_colormap(z: float, transparent: bool = True):
    """shifted bwr colormap"""
    if (z < 0) or (z > 1):
        raise ValueError('z must be between 0 and 1')

    cdict1 = {'red': ((0.0, max(-2*z+1, 0), max(-2*z+1, 0)),
                      (z,   1.0, 1.0),
                      (1.0, 1.0, 1.0)),

              'green': ((0.0, max(-2*z+1, 0), max(-2*z+1, 0)),
                        (z,   1.0, 1.0),
                        (1.0, max(2*z-1,0),  max(2*z-1,0))),

              'blue': ((0.0, 1.0, 1.0),
                       (z,   1.0, 1.0),
                       (1.0, max(2*z-1,0), max(2*z-1,0))),
              }
    if transparent:
        cdict1['alpha'] = ((0.0, 1-max(-2*z+1, 0), 1-max(-2*z+1, 0)),
                           (z,   0.0, 0.0),
                           (1.0, 1-max(2*z-1,0),  1-max(2*z-1,0)))

    return LinearSegmentedColormap('shifted_rwb', cdict1)

cmap =  shift_zero_bwr_colormap(.3)

x = np.arange(0, np.pi, 0.1)
y = np.arange(0, 2*np.pi, 0.1)
X, Y = np.meshgrid(x, y)
Z = np.cos(X) * np.sin(Y) * 5 + 5
plt.plot([0, 10*np.pi], [0, 20*np.pi], color='c', lw=20, zorder=-3)
plt.imshow(Z, interpolation='nearest', origin='lower', cmap=cmap)
plt.colorbar()
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文