Matplotlib 2 subparcelas, 1 barra de cores

235

Passei muito tempo pesquisando como obter duas subparcelas para compartilhar o mesmo eixo y com uma única barra de cores compartilhada entre as duas no Matplotlib.

O que estava acontecendo era que, quando eu chamei a colorbar()função em um subplot1ou outro subplot2, ele dimensionaria automaticamente a plotagem de forma que a barra de cores mais a plotagem caíssem dentro da caixa delimitadora da 'subtrama', fazendo com que as duas plotagens lado a lado fossem duas muito diferentes. tamanhos.

Para contornar isso, tentei criar uma terceira subtrama que eu hackeei para renderizar nenhuma plotagem com apenas uma barra de cores presente. O único problema é que agora as alturas e larguras das duas parcelas são desiguais, e não consigo descobrir como fazê-la parecer correta.

Aqui está o meu código:

from __future__ import division
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import patches
from matplotlib.ticker import NullFormatter

# SIS Functions
TE = 1 # Einstein radius
g1 = lambda x,y: (TE/2) * (y**2-x**2)/((x**2+y**2)**(3/2)) 
g2 = lambda x,y: -1*TE*x*y / ((x**2+y**2)**(3/2))
kappa = lambda x,y: TE / (2*np.sqrt(x**2+y**2))

coords = np.linspace(-2,2,400)
X,Y = np.meshgrid(coords,coords)
g1out = g1(X,Y)
g2out = g2(X,Y)
kappaout = kappa(X,Y)
for i in range(len(coords)):
    for j in range(len(coords)):
        if np.sqrt(coords[i]**2+coords[j]**2) <= TE:
            g1out[i][j]=0
            g2out[i][j]=0

fig = plt.figure()
fig.subplots_adjust(wspace=0,hspace=0)

# subplot number 1
ax1 = fig.add_subplot(1,2,1,aspect='equal',xlim=[-2,2],ylim=[-2,2])
plt.title(r"$\gamma_{1}$",fontsize="18")
plt.xlabel(r"x ($\theta_{E}$)",fontsize="15")
plt.ylabel(r"y ($\theta_{E}$)",rotation='horizontal',fontsize="15")
plt.xticks([-2.0,-1.5,-1.0,-0.5,0,0.5,1.0,1.5])
plt.xticks([-2.0,-1.5,-1.0,-0.5,0,0.5,1.0,1.5])
plt.imshow(g1out,extent=(-2,2,-2,2))
plt.axhline(y=0,linewidth=2,color='k',linestyle="--")
plt.axvline(x=0,linewidth=2,color='k',linestyle="--")
e1 = patches.Ellipse((0,0),2,2,color='white')
ax1.add_patch(e1)

# subplot number 2
ax2 = fig.add_subplot(1,2,2,sharey=ax1,xlim=[-2,2],ylim=[-2,2])
plt.title(r"$\gamma_{2}$",fontsize="18")
plt.xlabel(r"x ($\theta_{E}$)",fontsize="15")
ax2.yaxis.set_major_formatter( NullFormatter() )
plt.axhline(y=0,linewidth=2,color='k',linestyle="--")
plt.axvline(x=0,linewidth=2,color='k',linestyle="--")
plt.imshow(g2out,extent=(-2,2,-2,2))
e2 = patches.Ellipse((0,0),2,2,color='white')
ax2.add_patch(e2)

# subplot for colorbar
ax3 = fig.add_subplot(1,1,1)
ax3.axis('off')
cbar = plt.colorbar(ax=ax2)

plt.show()
astromax
fonte

Respostas:

319

Basta colocar a barra de cores em seu próprio eixo e usar subplots_adjustpara abrir espaço para ela.

Como um exemplo rápido:

import numpy as np
import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows=2, ncols=2)
for ax in axes.flat:
    im = ax.imshow(np.random.random((10,10)), vmin=0, vmax=1)

fig.subplots_adjust(right=0.8)
cbar_ax = fig.add_axes([0.85, 0.15, 0.05, 0.7])
fig.colorbar(im, cax=cbar_ax)

plt.show()

insira a descrição da imagem aqui

Observe que o intervalo de cores será definido pela última imagem plotada (que deu origem im), mesmo que o intervalo de valores seja definido por vmine vmax. Se outro gráfico tiver, por exemplo, um valor máximo mais alto, os pontos com valores mais altos que o máximo de imserão exibidos em cores uniformes.

Joe Kington
fonte
4
O ImageGrid também é muito útil para esse propósito exato.
Phillip Nuvem
5
se você precisar usar tight_layout (), desejará fazer tudo após subplots_adjust após tight_layout e depois ajustar as coordenadas para subplots_adjust e add_axes manualmente.
user1748155
2
Como posso ter uma barra de cores única para dois gráficos de dispersão diferentes que já tenho? Eu tentei acima, mas não sei como substituir "im" por variáveis ​​apropriadas. Let dizer os diagramas de dispersão são Plot1 = pylib.scatter (x, y, z) e Plot2 = pylib.scatter (a, b, c)
Rotail
46
Isso pode ter sido óbvio para os outros, mas eu queria ressaltar que, para que a barra de cores represente com precisão a cor em todas as plotagens, os argumentos vmine vmaxsão críticos. Eles controlam o intervalo de cores de cada subtrama. Se você tiver dados reais, pode ser necessário passar por isso para encontrar os valores mínimo e máximo primeiro.
James Owers
2
se o intervalo de valores dos gráficos for diferente, o intervalo da barra de cores mostrará apenas o intervalo do último gráfico, certo? alguma sugestão?
Lukas
132

Você pode simplificar o código de Joe Kington usando o axparâmetro de figure.colorbar()com uma lista de eixos. A partir da documentação :

machado

Nenhuma objeto (s) de eixos pai dos quais o espaço para um novo eixo da barra de cores será roubado. Se uma lista de eixos for fornecida, todos serão redimensionados para dar espaço aos eixos da barra de cores.

import numpy as np
import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows=2, ncols=2)
for ax in axes.flat:
    im = ax.imshow(np.random.random((10,10)), vmin=0, vmax=1)

fig.colorbar(im, ax=axes.ravel().tolist())

plt.show()

1

abevieiramota
fonte
4
Esta solução funcionou muito bem aqui e parece ser a mais fácil.
KKND
8
Se você alterar nrows para 1, os dois gráficos serão mais detalhados do que a barra de cores. então, como resolver esse problema?
19716 Jin
6
Pena que não funciona com tight_layout, mas mesmo assim uma boa solução.
Mark
1
Só para lembrar ... Adoro esta solução! Tinha que ser cearense!
iury Simoes-Sousa
1
A parte crucial desta resposta é fig.colorbar(im, ax=axes.ravel().tolist()). Se você omitir ax=axes.ravel().tolist(), a barra de cores será colocada dentro de uma subtrama.
nyanpasu64
55

Esta solução não requer ajustes manuais dos locais dos eixos ou do tamanho da barra de cores, trabalha com layouts de várias linhas e de uma linha e trabalha com tight_layout(). É adaptado de um exemplo da galeria , usando ImageGrida caixa de ferramentas AxesGrid do matplotlib .

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

# Set up figure and image grid
fig = plt.figure(figsize=(9.75, 3))

grid = ImageGrid(fig, 111,          # as in plt.subplot(111)
                 nrows_ncols=(1,3),
                 axes_pad=0.15,
                 share_all=True,
                 cbar_location="right",
                 cbar_mode="single",
                 cbar_size="7%",
                 cbar_pad=0.15,
                 )

# Add data to image grid
for ax in grid:
    im = ax.imshow(np.random.random((10,10)), vmin=0, vmax=1)

# Colorbar
ax.cax.colorbar(im)
ax.cax.toggle_label(True)

#plt.tight_layout()    # Works, but may still require rect paramater to keep colorbar labels visible
plt.show()

grade de imagem

spinup
fonte
Duplo + 1, esta é uma ótima abordagem
Brett
Na verdade, funciona com tight_layout, mas não tenho idéia de como adicionar um rótulo a essa barra de cores. Ele não aceita o rótulo, o título, o texto ... qualquer coisa! E os documentos não ajudam muito.
TomCho 21/07
3
@TomCho Para definir um rótulo, você pode pegar a alça do colorbar quando você instancia, como: thecb = ax.cax.colorbar(im). Então você pode fazerthecb.set_label_text("foo")
spinup
1
Como mudar o mapa de cores?
Sigur
1
@Sigur Eu tenho certeza que você já descobriu, mas para outros, você pode alterar o cmap ao declarar im: im = ax.imshow (data, vmin = 0, vmax = 1, cmap = 'your_cmap_here')
Shaun Lowis
38

O uso make_axesé ainda mais fácil e oferece um resultado melhor. Também oferece possibilidades de personalizar o posicionamento da barra de cores. Observe também a opção de subplotscompartilhar os eixos x e y.

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

fig, axes = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True)
for ax in axes.flat:
    im = ax.imshow(np.random.random((10,10)), vmin=0, vmax=1)

cax,kw = mpl.colorbar.make_axes([ax for ax in axes.flat])
plt.colorbar(im, cax=cax, **kw)

plt.show()

kch
fonte
7
Este método não funciona quando a subparcela não é quadrada. Se você alterar nrows=1, a barra de cores se tornará maior que as subparcelas novamente.
Wesley Tansey
Quais são os padrões do seu matplotlib? parece ótimo!
Rafaelvalle
18

Como iniciante que tropeçou nesse tópico, eu gostaria de adicionar uma adaptação python-por-manequins da resposta muito legal de abevieiramota (porque estou no nível em que precisei procurar 'ravel' para descobrir o que o código deles estava funcionando):

import numpy as np
import matplotlib.pyplot as plt

fig, ((ax1,ax2,ax3),(ax4,ax5,ax6)) = plt.subplots(2,3)

axlist = [ax1,ax2,ax3,ax4,ax5,ax6]

first = ax1.imshow(np.random.random((10,10)), vmin=0, vmax=1)
third = ax3.imshow(np.random.random((12,12)), vmin=0, vmax=1)

fig.colorbar(first, ax=axlist)

plt.show()

Muito menos pitônico, muito mais fácil para noobs como eu ver o que realmente está acontecendo aqui.

RChapman
fonte
17

Como apontado em outras respostas, a idéia é geralmente definir um eixo para a barra de cores residir. Existem várias maneiras de fazer isso; um que ainda não tenha sido mencionado seria especificar diretamente os eixos da barra de cores na criação da subtrama plt.subplots(). A vantagem é que a posição dos eixos não precisa ser definida manualmente e, em todos os casos com aspecto automático, a barra de cores terá exatamente a mesma altura das subparcelas. Mesmo em muitos casos em que as imagens são usadas, o resultado será satisfatório, como mostrado abaixo.

Ao usar plt.subplots(), o uso do gridspec_kwargumento permite tornar os eixos da barra de cores muito menores que os outros eixos.

fig, (ax, ax2, cax) = plt.subplots(ncols=3,figsize=(5.5,3), 
                  gridspec_kw={"width_ratios":[1,1, 0.05]})

Exemplo:

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)

fig, (ax, ax2, cax) = plt.subplots(ncols=3,figsize=(5.5,3), 
                  gridspec_kw={"width_ratios":[1,1, 0.05]})
fig.subplots_adjust(wspace=0.3)
im  = ax.imshow(np.random.rand(11,8), vmin=0, vmax=1)
im2 = ax2.imshow(np.random.rand(11,8), vmin=0, vmax=1)
ax.set_ylabel("y label")

fig.colorbar(im, cax=cax)

plt.show()

insira a descrição da imagem aqui

Isso funciona bem, se o aspecto das plotagens for escalonado automaticamente ou as imagens forem reduzidas devido ao aspecto na direção da largura (como acima). Se, no entanto, as imagens forem mais amplas que altas, o resultado seria o seguinte, o que pode ser indesejável.

insira a descrição da imagem aqui

Uma solução para fixar a altura da barra de cores à altura da subparcela seria usar mpl_toolkits.axes_grid1.inset_locator.InsetPositionpara definir os eixos da barra de cores em relação aos eixos da subparcela da imagem.

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition

fig, (ax, ax2, cax) = plt.subplots(ncols=3,figsize=(7,3), 
                  gridspec_kw={"width_ratios":[1,1, 0.05]})
fig.subplots_adjust(wspace=0.3)
im  = ax.imshow(np.random.rand(11,16), vmin=0, vmax=1)
im2 = ax2.imshow(np.random.rand(11,16), vmin=0, vmax=1)
ax.set_ylabel("y label")

ip = InsetPosition(ax2, [1.05,0,0.05,1]) 
cax.set_axes_locator(ip)

fig.colorbar(im, cax=cax, ax=[ax,ax2])

plt.show()

insira a descrição da imagem aqui

ImportanceOfBeingErnest
fonte
Não tenho certeza se posso perguntar isso aqui, mas existe uma maneira de implementar esta solução usando ax = fig.add_subplot()? Estou perguntando porque não consigo descobrir como usá-lo com o mapa base.
Lanadaquenada
1
@lanadaquenada Sim, isso é possível, mas você precisaria fornecer um GridSpecpara add_subplot()nesse caso.
ImportanceOfBeingErnest
10

A solução de usar uma lista de eixos por abevieiramota funciona muito bem até você usar apenas uma linha de imagens, conforme indicado nos comentários. Usando uma proporção razoável para obter figsizeajuda, mas ainda está longe de ser perfeito. Por exemplo:

import numpy as np
import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(9.75, 3))
for ax in axes.flat:
    im = ax.imshow(np.random.random((10,10)), vmin=0, vmax=1)

fig.colorbar(im, ax=axes.ravel().tolist())

plt.show()

1 x 3 matriz de imagens

A função barra de cores fornece o shrinkparâmetro que é um fator de escala para o tamanho dos eixos da barra de cores. Requer alguma tentativa e erro manual. Por exemplo:

fig.colorbar(im, ax=axes.ravel().tolist(), shrink=0.75)

Conjunto de imagens 1 x 3 com barra de cores reduzida

spinup
fonte
4

Para adicionar à excelente resposta de @ abevieiramota, você pode obter o euqivalente de tight_layout com constrained_layout. Você ainda terá grandes espaços horizontais se usar em imshowvez da pcolormeshrazão de aspecto de 1: 1 imposta por imshow.

import numpy as np
import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows=2, ncols=2, constrained_layout=True)
for ax in axes.flat:
    im = ax.pcolormesh(np.random.random((10,10)), vmin=0, vmax=1)

fig.colorbar(im, ax=axes.flat)
plt.show()

insira a descrição da imagem aqui

Jody Klymak
fonte
1

Percebi que quase todas as soluções postadas envolviam ax.imshow(im, ...)e não normalizavam as cores exibidas na barra de cores para as várias sub-configurações. O immapeável é extraído da última instância, mas e se os valores dos múltiplos im-s forem diferentes? (Suponho que esses mapas mapeáveis ​​sejam tratados da mesma maneira que os conjuntos de contorno e superfície). Eu tenho um exemplo usando um gráfico de superfície 3d abaixo que cria duas barras de cores para uma subparte 2x2 (uma barra de cores por uma linha) ) Embora a pergunta solicite explicitamente um arranjo diferente, acho que o exemplo ajuda a esclarecer algumas coisas. plt.subplots(...)Infelizmente, ainda não encontrei uma maneira de fazer isso usando os eixos 3D.

Exemplo de Gráfico

Se eu pudesse posicionar as barras de cores de uma maneira melhor ... (Provavelmente existe uma maneira muito melhor de fazer isso, mas pelo menos não deve ser muito difícil de seguir.)

import matplotlib
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.mplot3d import Axes3D

cmap = 'plasma'
ncontours = 5

def get_data(row, col):
    """ get X, Y, Z, and plot number of subplot
        Z > 0 for top row, Z < 0 for bottom row """
    if row == 0:
        x = np.linspace(1, 10, 10, dtype=int)
        X, Y = np.meshgrid(x, x)
        Z = np.sqrt(X**2 + Y**2)
        if col == 0:
            pnum = 1
        else:
            pnum = 2
    elif row == 1:
        x = np.linspace(1, 10, 10, dtype=int)
        X, Y = np.meshgrid(x, x)
        Z = -np.sqrt(X**2 + Y**2)
        if col == 0:
            pnum = 3
        else:
            pnum = 4
    print("\nPNUM: {}, Zmin = {}, Zmax = {}\n".format(pnum, np.min(Z), np.max(Z)))
    return X, Y, Z, pnum

fig = plt.figure()
nrows, ncols = 2, 2
zz = []
axes = []
for row in range(nrows):
    for col in range(ncols):
        X, Y, Z, pnum = get_data(row, col)
        ax = fig.add_subplot(nrows, ncols, pnum, projection='3d')
        ax.set_title('row = {}, col = {}'.format(row, col))
        fhandle = ax.plot_surface(X, Y, Z, cmap=cmap)
        zz.append(Z)
        axes.append(ax)

## get full range of Z data as flat list for top and bottom rows
zz_top = zz[0].reshape(-1).tolist() + zz[1].reshape(-1).tolist()
zz_btm = zz[2].reshape(-1).tolist() + zz[3].reshape(-1).tolist()
## get top and bottom axes
ax_top = [axes[0], axes[1]]
ax_btm = [axes[2], axes[3]]
## normalize colors to minimum and maximum values of dataset
norm_top = matplotlib.colors.Normalize(vmin=min(zz_top), vmax=max(zz_top))
norm_btm = matplotlib.colors.Normalize(vmin=min(zz_btm), vmax=max(zz_btm))
cmap = cm.get_cmap(cmap, ncontours) # number of colors on colorbar
mtop = cm.ScalarMappable(cmap=cmap, norm=norm_top)
mbtm = cm.ScalarMappable(cmap=cmap, norm=norm_btm)
for m in (mtop, mbtm):
    m.set_array([])

# ## create cax to draw colorbar in
# cax_top = fig.add_axes([0.9, 0.55, 0.05, 0.4])
# cax_btm = fig.add_axes([0.9, 0.05, 0.05, 0.4])
cbar_top = fig.colorbar(mtop, ax=ax_top, orientation='vertical', shrink=0.75, pad=0.2) #, cax=cax_top)
cbar_top.set_ticks(np.linspace(min(zz_top), max(zz_top), ncontours))
cbar_btm = fig.colorbar(mbtm, ax=ax_btm, orientation='vertical', shrink=0.75, pad=0.2) #, cax=cax_btm)
cbar_btm.set_ticks(np.linspace(min(zz_btm), max(zz_btm), ncontours))

plt.show()
plt.close(fig)
## orientation of colorbar = 'horizontal' if done by column

fonte
Se os valores dos múltiplos ims forem diferentes, eles não deverão usar a mesma barra de cores, para que a pergunta original não se aplique realmente.
spinup
0

Este tópico é bem abordado, mas eu ainda gostaria de propor outra abordagem em uma filosofia um pouco diferente.

É um pouco mais complexo de configurar, mas permite (na minha opinião) um pouco mais de flexibilidade. Por exemplo, pode-se jogar com as respectivas proporções de cada subparcela / barra de cores:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.gridspec import GridSpec

# Define number of rows and columns you want in your figure
nrow = 2
ncol = 3

# Make a new figure
fig = plt.figure(constrained_layout=True)

# Design your figure properties
widths = [3,4,5,1]
gs = GridSpec(nrow, ncol + 1, figure=fig, width_ratios=widths)

# Fill your figure with desired plots
axes = []
for i in range(nrow):
    for j in range(ncol):
        axes.append(fig.add_subplot(gs[i, j]))
        im = axes[-1].pcolormesh(np.random.random((10,10)))

# Shared colorbar    
axes.append(fig.add_subplot(gs[:, ncol]))
fig.colorbar(im, cax=axes[-1])

plt.show()

insira a descrição da imagem aqui

Enzoupi
fonte