# python tracer Définition du point milieu d'une palette de couleurs dans matplotlib

## tracer cercle python matplotlib (7)

Je sais que c'est en retard pour le jeu, mais je viens de passer par ce processus et est venu avec une solution qui est peut-être moins robuste que la sous-classification normaliser, mais beaucoup plus simple. J'ai pensé que ce serait bien de le partager ici pour la postérité.

### La fonction

``````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 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.
'''
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
``````

### Un exemple

``````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",

# 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([])
``````

### Résultats de l'exemple:

Je veux définir le point central d'une palette de couleurs, c'est-à-dire que mes données vont de -5 à 10, je veux que zéro soit le milieu. Je pense que la façon de le faire est de normaliser et d'utiliser la norme, mais je n'ai trouvé aucun exemple et ce n'est pas clair pour moi, ce que je dois exactement mettre en œuvre.

Cette solution est inspirée d'une classe portant le même nom à partir de cette page

Ici, je crée une sous-classe de `Normalize` suivie d'un exemple minimal.

``````import scipy as sp
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]

vals = sp.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()
``````

Résultat:

Et le même exemple avec seulement des données positives `vals = sp.array([[1, 3], [6, 10]])`

Pour résumer, cette norme a les propriétés suivantes:

• Le point central obtiendra la couleur du milieu.
• Les plages supérieures et inférieures seront redimensionnées de la même manière, de sorte qu'avec la bonne palette de couleurs, la saturation des couleurs corresponde à la distance du milieu.
• La barre de couleurs n'affichera que les couleurs qui apparaissent sur l'image.
• Semble fonctionner `vmin` même si `vmin` est plus grand que le `midpoint` (n'a pas testé tous les cas de bord cependant).

J'ai eu un problème similaire, mais je voulais que la valeur la plus élevée soit entièrement rouge et que les valeurs faibles du bleu soient coupées, ce qui donne l'impression que le fond de la barre de couleurs a été coupé. Cela a fonctionné pour moi (inclut la transparence facultative):

``````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()
``````

Voici une solution sous-classe Normaliser. Pour l'utiliser

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

Voici la classe:

``````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:
result = ma.array(np.clip(result.filled(vmax), vmin, vmax),

# 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

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 * (val - 0.5)
if val < 0:
return  val*abs(vmin-midpoint) + midpoint
else:
return  val*abs(vmax-midpoint) + midpoint
``````

J'utilisais l'excellente réponse de Paul H, mais j'ai rencontré un problème parce que certaines de mes données allaient de négatives à positives, tandis que d'autres variaient de 0 à positif ou de négatif à 0; dans les deux cas, je voulais que 0 soit coloré en blanc (le milieu de la palette de couleurs que j'utilise). Avec l'implémentation existante, si votre valeur `midpoint` est égale à 1 ou 0, les mappages d'origine n'ont pas été écrasés. Vous pouvez voir cela dans l'image suivante: La 3ème colonne semble correcte, mais la zone bleu foncé dans la 2ème colonne et la zone rouge foncé dans les colonnes restantes sont toutes supposées être blanches (leurs valeurs de données sont en fait 0). Utiliser ma solution me donne: Ma fonction est essentiellement la même que celle de Paul H, avec mes modifications au début de la boucle `for` :

``````    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://.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: J'ai encore rencontré un problème similaire lorsque certaines de mes données allaient d'une petite valeur positive à une valeur positive plus grande, où les valeurs très basses étaient colorées en rouge au lieu de blanc. Je l'ai corrigé en ajoutant la ligne `Edit #2` dans le code ci-dessus.

Je ne sais pas si vous cherchez toujours une réponse. Pour moi, essayer de sous- `Normalize` a échoué. Je me suis donc concentré sur la création manuelle d'un nouvel ensemble de données, de coches et d'étiquettes pour obtenir l'effet que je recherche.

J'ai trouvé le module d' `scale` dans matplotlib qui a une classe utilisée pour transformer les lignes par les règles 'syslog', donc je l'utilise pour transformer les données. Ensuite, je redimensionne les données pour qu'elles passent de 0 à 1 (ce que `Normalize` habituellement), mais je redimensionne les nombres positifs différemment des nombres négatifs. C'est parce que votre vmax et vmin pourraient ne pas être les mêmes, donc .5 -> 1 pourrait couvrir une gamme positive plus grande que .5 -> 0, la gamme négative fait. Il m'a été plus facile de créer une routine pour calculer les valeurs de tick et d'étiquette.

Voici le code et un exemple de 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))
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))
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()
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')
``````

N'hésitez pas à ajuster les "constantes" (par exemple `VMAX` ) en haut du script pour confirmer qu'il se comporte bien.

Il est plus facile d'utiliser simplement les arguments `vmin` et `vmax` pour `imshow` (en supposant que vous travaillez avec des données d'image) plutôt que de sous- `matplotlib.colors.Normalize` .

Par exemple

``````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()
``````