Related
I want to create a heatmap with seaborn, similar to this (with the following code):
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
# Create data
df = pd.DataFrame(np.random.random((5,5)), columns=["a","b","c","d","e"])
# Default heatmap
ax = sns.heatmap(df)
plt.show()
I'd also like to add a new variable (lets say new_var = pd.DataFrame(np.random.random((5,1)), columns=["new variable"])), such as that the values (and possibly the spine and ticks as well) of the y-axis are colored according to the new variable and a second color bar plotted in the same plot to represent the colors of the y-axis values. How can I do that?
This uses the new values to color the y-ticks and the y-tick labels and adds the associated colorbar.
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
import pandas as pd
import numpy as np
# Create data
df = pd.DataFrame(np.random.random((5,5)), columns=["a","b","c","d","e"])
# Default heatmap
ax = sns.heatmap(df)
new_var = pd.DataFrame(np.random.random((5,1)), columns=["new variable"])
# Create the colorbar for y-ticks and labels
norm = plt.Normalize(new_var.min(), new_var.max())
cmap = matplotlib.cm.get_cmap('turbo')
yticks_locations = ax.get_yticks()
yticks_labels = df.index.values
#hide original ticks
ax.tick_params(axis='y', left=False)
ax.set_yticklabels([])
for var, ytick_loc, ytick_label in zip(new_var.values, yticks_locations, yticks_labels):
color = cmap(norm(float(var)))
ax.annotate(ytick_label, xy=(1, ytick_loc), xycoords='data', xytext=(-0.4, ytick_loc),
arrowprops=dict(arrowstyle="-", color=color, lw=1), zorder=0, rotation=90, color=color)
# Add colorbar for y-tick colors
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
cb = ax.figure.colorbar(sm)
# Match the seaborn style
cb.outline.set_visible(False)
I found your problem interesting, and inspired by the unanswered comment above:
How do you change the second colorbar position? For example, one on top the other on bottom sides. - Py-ser
I decided to spend a while doing some tests. After a little digging i find that cbar_kws={"orientation": "horizontal"} is the argument for sns.heatmap that makes the colorbars horizontal.
Borrowing the code from the solution and making some changes, you can format your plot the way you want as in:
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
import pandas as pd
import numpy as np
# Create data
df = pd.DataFrame(np.random.random((5,5)), columns=["a","b","c","d","e"])
# Default heatmap
ax = sns.heatmap(df, cbar_kws={"orientation": "horizontal"}, square = False, annot = True)
new_var = pd.DataFrame(np.random.random((5,1)), columns=["new variable"])
# Create the colorbar for y-ticks and labels
norm = plt.Normalize(new_var.min(), new_var.max())
cmap = matplotlib.cm.get_cmap('turbo')
yticks_locations = ax.get_yticks()
yticks_labels = df.index.values
#hide original ticks
ax.tick_params(axis='y', left=False)
ax.set_yticklabels([])
for var, ytick_loc, ytick_label in zip(new_var.values, yticks_locations, yticks_labels):
color = cmap(norm(float(var)))
ax.annotate(ytick_label, xy=(1, ytick_loc), xycoords='data', xytext=(-0.4, ytick_loc),
arrowprops=dict(arrowstyle="-", color=color, lw=1), zorder=0, rotation=90, color=color)
# Add colorbar for y-tick colors
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
cb = ax.figure.colorbar(sm)
# Match the seaborn style
cb.outline.set_visible(False)
Also, you will notice that I listed the values related to each cell in the heatmap, but just out of curiosity to make it clearer to check that everything was working as expected.
I'm still not very happy with the shape/size of the horizontal colorbar, but I'll keep testing and update any progress by editing this answer!
==========================================
EDIT
just to keep track of the updates, first i tried to change just some parameters of seaborn's heatmap function but wouldn't consider this a major improvement on the task... by adding
ax = sns.heatmap(df, cbar_kws = dict(use_gridspec=True, location="top", shrink =0.6), square = True, annot = True)
I end up with:
I did get to separate the colormap using the matplotlib subplot routine and honestly i believe this is the right way given the parameter control that is possible to get here, by:
# Define two rows for subplots
fig, (cax, ax) = plt.subplots(nrows=2, figsize=(5,5.025), gridspec_kw={"height_ratios":[0.025, 1]})
# Default heatmap
ax = sns.heatmap(df, cbar=False, annot = True)
# colorbar
fig.colorbar(ax.get_children()[0], cax=cax, orientation="horizontal")
plt.show()
I obtained:
Which is still not the prettiest graph I've ever made, but now the position and size of the heatmap can be edited normally within the plt.subplots subroutines that give absolute control over these parameters.
I have a matplotlib bar chart, which bars are colored according to some rules through a colormap. I need a colorbar on the right of the main axes, so I added a new axes with
fig, (ax, ax_cbar) = plt.subplots(1,2)
and managed to draw my color bar in the ax_bar axes, while I have my data displayed in the ax axes. Now I need to reduce the width of the ax_bar, because it looks like this:
How can I do?
Using subplots will always divide your figure equally. You can manually divide up your figure in a number of ways. My preferred method is using subplot2grid.
In this example, we are setting the figure to have 1 row and 10 columns. We then set ax to be the start at row,column = (0,0) and have a width of 9 columns. Then set ax_cbar to start at (0,9) and has by default a width of 1 column.
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8,6))
num_columns = 10
ax = plt.subplot2grid((1,num_columns), (0,0), colspan=num_columns-1)
ax_cbar = plt.subplot2grid((1,num_columns), (0,num_columns-1))
The ususal way to add a colorbar is by simply putting it next to the axes:
fig.colorbar(sm)
where fig is the figure and sm is the scalar mappable to which the colormap refers. In the case of the bars, you need to create this ScalarMappable yourself. Apart from that there is no need for complex creation of multiple axes.
import matplotlib.pyplot as plt
import matplotlib.colors
import numpy as np
fig , ax = plt.subplots()
x = [0,1,2,3]
y = np.array([34,40,38,50])*1e3
norm = matplotlib.colors.Normalize(30e3, 60e3)
ax.bar(x,y, color=plt.cm.plasma_r(norm(y)) )
ax.axhline(4.2e4, color="gray")
ax.text(0.02, 4.2e4, "42000", va='center', ha="left", bbox=dict(facecolor="w",alpha=1),
transform=ax.get_yaxis_transform())
sm = plt.cm.ScalarMappable(cmap=plt.cm.plasma_r, norm=norm)
sm.set_array([])
fig.colorbar(sm)
plt.show()
If you do want to create a special axes for the colorbar yourself, the easiest method would be to set the width already inside the call to subplots:
fig , (ax, cax) = plt.subplots(ncols=2, gridspec_kw={"width_ratios" : [10,1]})
and later put the colorbar to the cax axes,
fig.colorbar(sm, cax=cax)
Note that the following questions have been asked for this homework assignment already:
Point picker event_handler drawing line and displaying coordinates in matplotlib
Matplotlib's widget to select y-axis value and change barplot
Display y axis value horizontal line drawn In bar chart
How to change colors automatically once a parameter is changed
Interactively Re-color Bars in Matplotlib Bar Chart using Confidence Intervals
1) I am not able to see the text-based xticks which are stored as list in the variable x. When I have only one single column based bar plot, I can see the xticks as text but not for more.
2)how can I control the font properties of xticks and the values in y axis?
Thank you.
import matplotlib.pyplot as plt
import pylab as pl
import numpy as np
#load text and columns into different variables
data = np.genfromtxt('a', names=True, dtype=None, usecols=("X", "N2", "J2", "V2", "asd", "xyz"))
x = data['X']
n = data['N2']
j = data['J2']
v = data['V2']
#make x axis string based labels
r=np.arange(1,25,1.5)
plt.xticks(r,x) #make sure dimension of x and n matches
plt.figure(figsize=(3.2,2), dpi=300, linewidth=3.0)
ax = plt.subplot(111)
ax.bar(r,v,width=0.9,color='red',edgecolor='black', lw=0.5, align='center')
plt.axhline(y=0,linewidth=1.0,color='black') #horizontal line at y=0
plt.axis([0.5,16.5,-0.4,0.20])
ax.bar(r,j,width=0.6,color='green',edgecolor='black', lw=0.5, align='center')
ax.bar(r,n,width=0.3,color='blue',edgecolor='black', lw=0.5, align='center')
plt.axhline(y=0,linewidth=1,color='black') #horizontal line at y=0
plt.axis([0.5,24.5,-0.36,0.15])
plt.savefig('fig',dpi=300,format='png',orientation='landscape')
The way you're doing it, you just need to move the call to plt.xticks(r,x) to somewhere after you create the figure you're working on. Otherwise pyplot will create a new figure for you.
However, I would also consider switching to the more explicit object-oriented interface to matplotlib.
This way you'd use:
fig, ax = plt.subplots(1,1) # your only call to plt
ax.bar(r,v,width=0.9,color='red',edgecolor='black', lw=0.5, align='center')
ax.bar(r,j,width=0.6,color='green',edgecolor='black', lw=0.5, align='center')
ax.bar(r,n,width=0.3,color='blue',edgecolor='black', lw=0.5, align='center')
ax.set_xticks(r)
ax.set_xticklabels(x)
ax.axhline(y=0,linewidth=1,color='black')
fig.savefig('fig',dpi=300,format='png',orientation='landscape')
# or use plt.show() to see the figure interactively or inline, depending on backend
# (see Joe Kington's comment below)
Here is an example that reproduces my problem:
import matplotlib.pyplot as plt
import numpy as np
data1,data2,data3,data4 = np.random.random(100),np.random.random(100),np.random.random(100),np.random.random(100)
fig,ax = plt.subplots()
ax.plot(data1)
ax.plot(data2)
ax.plot(data3)
ax2 = ax.twinx()
ax2.plot(data4)
plt.grid('on')
ax.legend(['1','2','3'], loc='center')
ax2.legend(['4'], loc=1)
How can I get the legend in the center to plot on top of the lines?
To get exactly what you have asked for, try the following. Note I have modified your code to define the labels when you generate the plot and also the colors so you don't get a repeated blue line.
import matplotlib.pyplot as plt
import numpy as np
data1,data2,data3,data4 = (np.random.random(100),
np.random.random(100),
np.random.random(100),
np.random.random(100))
fig,ax = plt.subplots()
ax.plot(data1, label="1", color="k")
ax.plot(data2, label="2", color="r")
ax.plot(data3, label="3", color="g")
ax2 = ax.twinx()
ax2.plot(data4, label="4", color="b")
# First get the handles and labels from the axes
handles1, labels1 = ax.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
# Add the first legend to the second axis so it displaysys 'on top'
first_legend = plt.legend(handles1, labels1, loc='center')
ax2.add_artist(first_legend)
# Add the second legend as usual
ax2.legend(handles2, labels2)
plt.show()
Now I will add that it would be clearer if you just use a single legend adding all the lines to that. This is described in this SO post and in the code above can easily be achieved with
ax2.legend(handles1+handles2, labels1+labels2)
But obviously you may have your own reasons for wanting two legends.
I'm developing a Web application and want to display a figure and its legend in different locations on the page. Which means I need to save the legend as a separate png file. Is this possible in Matplotlib in a more or less straightforward way?
This could work:
import pylab
fig = pylab.figure()
figlegend = pylab.figure(figsize=(3,2))
ax = fig.add_subplot(111)
lines = ax.plot(range(10), pylab.randn(10), range(10), pylab.randn(10))
figlegend.legend(lines, ('one', 'two'), 'center')
fig.show()
figlegend.show()
figlegend.savefig('legend.png')
You may limit the saved region of a figure to the bounding box of the legend using the bbox_inches argument to fig.savefig. Below to versions of a function which you can simply call with the legend you want to save as argument. You may either use the legend created in the original figure here (and remove it afterwards, legend.remove()) or you may create a new figure for the legend and simply use the function as it is.
Export legend boundingbox
In case the complete legend shall be saved, the bounding box supplied to the bbox_inches argument would be simply the transformed bounding box of the legend. This works well if the legend has no border around it.
import matplotlib.pyplot as plt
colors = ["crimson", "purple", "gold"]
f = lambda m,c: plt.plot([],[],marker=m, color=c, ls="none")[0]
handles = [f("s", colors[i]) for i in range(3)]
labels = colors
legend = plt.legend(handles, labels, loc=3, framealpha=1, frameon=False)
def export_legend(legend, filename="legend.png"):
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
export_legend(legend)
plt.show()
Export extended legend bounding box
If there is a border around the legend, the above solution may be suboptimal. In this case it makes sense to extend the bounding box by some pixels to include the border to its full.
import numpy as np
import matplotlib.pyplot as plt
colors = ["crimson", "purple", "gold"]
f = lambda m,c: plt.plot([],[],marker=m, color=c, ls="none")[0]
handles = [f("s", colors[i]) for i in range(3)]
labels = colors
legend = plt.legend(handles, labels, loc=3, framealpha=1, frameon=True)
def export_legend(legend, filename="legend.png", expand=[-5,-5,5,5]):
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent()
bbox = bbox.from_extents(*(bbox.extents + np.array(expand)))
bbox = bbox.transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
export_legend(legend)
plt.show()
use pylab.figlegend(..) and get_legend_handles_labels(..):
import pylab, numpy
x = numpy.arange(10)
# create a figure for the data
figData = pylab.figure()
ax = pylab.gca()
for i in xrange(3):
pylab.plot(x, x * (i+1), label='line %d' % i)
# create a second figure for the legend
figLegend = pylab.figure(figsize = (1.5,1.3))
# produce a legend for the objects in the other figure
pylab.figlegend(*ax.get_legend_handles_labels(), loc = 'upper left')
# save the two figures to files
figData.savefig("plot.png")
figLegend.savefig("legend.png")
It can be tricky though to get the size of the legend figure right in an automated manner.
It is possible to use axes.get_legend_handles_labels to get the legend handles and labels from one axes object and to use them to add them to an axes in a different figure.
# create a figure with one subplot
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([1,2,3,4,5], [1,2,3,4,5], 'r', label='test')
# save it *without* adding a legend
fig.savefig('image.png')
# then create a new image
# adjust the figure size as necessary
figsize = (3, 3)
fig_leg = plt.figure(figsize=figsize)
ax_leg = fig_leg.add_subplot(111)
# add the legend from the previous axes
ax_leg.legend(*ax.get_legend_handles_labels(), loc='center')
# hide the axes frame and the x/y labels
ax_leg.axis('off')
fig_leg.savefig('legend.png')
If for some reason you want to hide only the axes label, you can use:
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
or if, for some weirder reason, you want to hide the axes frame but not the axes labels you can use:
ax.set_frame_on(False)
ps: this answer has been adapted from my answer to a duplicate question
I've found that the easiest way is just to create your legend and then just turn off the axis with plt.gca().set_axis_off():
# Create a color palette
palette = dict(zip(['one', 'two'], ['b', 'g']))
# Create legend handles manually
handles = [mpl.patches.Patch(color=palette[x], label=x) for x in palette.keys()]
# Create legend
plt.legend(handles=handles)
# Get current axes object and turn off axis
plt.gca().set_axis_off()
plt.show()
This calculates the size of the legend automatically. If mode == 1, the code is similar to Steve Tjoa's answer, while mode == 2 is similar Andre Holzner's answer.
The loc parameter must be set to 'center' to make it work (but I do not know why this is necessary).
mode = 1
#mode = 2
import pylab
fig = pylab.figure()
if mode == 1:
lines = fig.gca().plot(range(10), pylab.randn(10), range(10), pylab.randn(10))
legend_fig = pylab.figure(figsize=(3,2))
legend = legend_fig.legend(lines, ('one', 'two'), 'center')
if mode == 2:
fig.gca().plot(range(10), pylab.randn(10), range(10), pylab.randn(10), label='asd')
legend_fig = pylab.figure()
legend = pylab.figlegend(*fig.gca().get_legend_handles_labels(), loc = 'center')
legend.get_frame().set_color('0.70')
legend_fig.canvas.draw()
legend_fig.savefig('legend_cropped.png',
bbox_inches=legend.get_window_extent().transformed(legend_fig.dpi_scale_trans.inverted()))
legend_fig.savefig('legend_original.png')
Original (uncropped) legend:
Cropped legend:
Inspired by Maxim and ImportanceOfBeingErnest's answers,
def export_legend(ax, filename="legend.pdf"):
fig2 = plt.figure()
ax2 = fig2.add_subplot()
ax2.axis('off')
legend = ax2.legend(*ax.get_legend_handles_labels(), frameon=False, loc='lower center', ncol=10,)
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
which allows me to save legend horizontally in a separate file. As an example
In November 2020, I tried almost everything on this post, but none worked for me. After struggling for a while, I found a solution that does what I want.
Pretend you want to draw a figure and a legend separately that looks like below (apparently I don't have enough reputation to embed pictures in a post; click the links to see the picture).
import matplotlib.pyplot as plt
%matplotlib inline
fig, ax = plt.subplots()
ax.plot([1, 2, 3], label="test1")
ax.plot([3, 2, 1], label="test2")
ax.legend()
target figure
You can separate the figure and the legend in two different ax objects:
fig, [ax1, ax2] = plt.subplots(1, 2)
ax1.plot([1, 2, 3], label="test1")
ax1.plot([3, 2, 1], label="test2")
ax2.plot([1, 2, 3], label="test1")
ax2.plot([3, 2, 1], label="test2")
h, l = ax2.get_legend_handles_labels()
ax2.clear()
ax2.legend(h, l, loc='upper left')
ax2.axis('off')
fixed figure 1
You can easily control where the legend should go:
fig, [ax1, ax2] = plt.subplots(2, 1)
ax1.plot([1, 2, 3], label="test1")
ax1.plot([3, 2, 1], label="test2")
ax2.plot([1, 2, 3], label="test1")
ax2.plot([3, 2, 1], label="test2")
h, l = ax2.get_legend_handles_labels()
ax2.clear()
ax2.legend(h, l, loc='upper left')
ax2.axis('off')
fixed figure 2
I'd like to add a small contribution for the specific case where your legend is customized such as here: https://matplotlib.org/3.1.1/gallery/text_labels_and_annotations/custom_legends.html
In that case, you might have to go for a different method. I've been exposed to that problem and the answers above did not work for me.
The code below sets-up the legend.
import cmocean
import matplotlib
from matplotlib.lines import Line2D
lightcmap = cmocean.tools.lighten(cmo.solar, 0.7)
custom_legend = []
custom_legend_strings=['no impact - high confidence', 'no impact - low confidence', 'impact - low confidence', 'impact - high confidence']
for nbre_classes in range(len(custom_legend_strings)):
custom_legend.append(Line2D([0], [0], color=lightcmap(nbre_classes/len(custom_legend_strings)), lw=4))
I think because this kind of legend is attached the axes, a little trick was necessary :
center the legend with a big font to make it take most of the available space and do not erase but set the axes to off :
fig,ax = plt.subplots(figsize=(10,10))
ax.legend(custom_legend,custom_legend_strings, loc = 10, fontsize=30)
plt.axis('off')
fig.savefig('legend.png', bbox_inches='tight')
The result is :
the result
I was not able to find exactly what I wanted in the existing answer so I implemented it. It wanted to generate a standalone legend without any figure attached nor visual "glitches". I came up with this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
palette = dict(zip(['one', 'two', 'tree', 'four'], ['b', 'g', 'r', 'k']))
def export_legend(palette, dpi="figure", filename="legend.png"):
# Create empty figure with the legend
handles = [Patch(color=c, label=l) for l, c in palette.items()]
fig = plt.figure()
legend = fig.gca().legend(handles=handles, framealpha=1, frameon=True)
# Render the legend
fig.canvas.draw()
# Export the figure, limiting the bounding box to the legend area,
# slighly extended to ensure the surrounding rounded corner box of
# is not cropped. Transparency is enabled, so it is not an issue.
bbox = legend.get_window_extent().padded(2)
bbox = bbox.transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi=dpi, transparent=True, bbox_inches=bbox)
# Delete the legend along with its temporary figure
plt.close(fig)
export_legend(palette, dpi=400)
Note that the surrounding background is transparent, so adding the legend on top of a figure should not have white "glitches" in the corners, nor issue of cropped border.
And if you don't want to save the image of disk, here is the trick !
DPI = 400
def export_legend(palette):
# Create empty figure with the legend
handles = [Patch(color=c, label=l) for l, c in palette.items()]
fig = plt.figure()
legend = fig.gca().legend(handles=handles, framealpha=1, frameon=True)
# Render the legend
fig.canvas.draw()
# Export the figure, limiting the bounding box to the legend area,
# slighly extended to ensure the surrounding rounded corner box of
# is not cropped. Transparency is enabled, so it is not an issue.
bbox = legend.get_window_extent().padded(2)
bbox_inches = bbox.transformed(fig.dpi_scale_trans.inverted())
bbox_inches = bbox.from_extents(np.round(bbox_inches.extents * 400) / 400)
io_buf = io.BytesIO()
fig.savefig(io_buf, format='rgba', dpi=DPI, transparent=True, bbox_inches=bbox_inches)
io_buf.seek(0)
img_raw = io_buf.getvalue()
img_size = (np.asarray(bbox_inches.bounds)[2:] * DPI).astype(int)
# Delete the legend along with its temporary figure
plt.close(fig)
return img_raw, img_size
The raw buffer can be read directly using PIL or whatever dealing with raw buffer.
So I was playing with this idea and simplest thing I have found is this (works with multiple axes):
def export_legend(filename="legend.png", fig=fig):
legend = fig.legend(framealpha=1)
fig2 = legend.figure
fig2.canvas.draw()
bbox = legend.get_window_extent().transformed(fig2.dpi_scale_trans.inverted())
fig2.savefig(filename, dpi="figure", bbox_inches=bbox, facecolor="w")
legend.remove() # removes legend from showing on plot
export_legend()
Output of a function (I hided labels with boxes):
fig is from fig, ax = plt.subplots()
If you want legend to still show on plot you can use (for example):
fig.legend(loc="upper right", bbox_to_anchor=(1, 1), bbox_transform=ax.transAxes)