Legend handle to an xarray plot - matplotlib

I cannot modify the legend of plot of a dataset made with xarray plotting function.
The code below returns No handles with labels found to put in legend.
import xarray as xr
import matplotlib.pyplot as plt
air = xr.tutorial.open_dataset("air_temperature").air
air.isel(lon=10, lat=[19, 21, 22]).plot.line(x="time", add_legend=True)
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))

You can use seaborn's sns.move_legend(), followed by plt.tight_layout(). sns.move_legend() is new in seaborn 0.11.2.
import xarray as xr
import matplotlib.pyplot as plt
import seaborn as sns
air = xr.tutorial.open_dataset("air_temperature").air
air.isel(lon=10, lat=[19, 21, 22]).plot.line(x="time", add_legend=True)
sns.move_legend(plt.gca(), loc='center left', bbox_to_anchor=(1, 0.5))
plt.tight_layout()
plt.show()
PS: If you don't want to import seaborn, you could copy the function from its source. You'll need to remove a reference to sns.axisgrid.Grid and import matplotlib as mpl; import inspect:
import matplotlib.pyplot as plt
import matplotlib as mpl
import inspect
import xarray as xr
def move_legend(obj, loc, **kwargs):
"""
Recreate a plot's legend at a new location.
Extracted from seaborn/utils.py
"""
if isinstance(obj, mpl.axes.Axes):
old_legend = obj.legend_
legend_func = obj.legend
elif isinstance(obj, mpl.figure.Figure):
if obj.legends:
old_legend = obj.legends[-1]
else:
old_legend = None
legend_func = obj.legend
else:
err = "`obj` must be a matplotlib Axes or Figure instance."
raise TypeError(err)
if old_legend is None:
err = f"{obj} has no legend attached."
raise ValueError(err)
# Extract the components of the legend we need to reuse
handles = old_legend.legendHandles
labels = [t.get_text() for t in old_legend.get_texts()]
# Extract legend properties that can be passed to the recreation method
# (Vexingly, these don't all round-trip)
legend_kws = inspect.signature(mpl.legend.Legend).parameters
props = {k: v for k, v in old_legend.properties().items() if k in legend_kws}
# Delegate default bbox_to_anchor rules to matplotlib
props.pop("bbox_to_anchor")
# Try to propagate the existing title and font properties; respect new ones too
title = props.pop("title")
if "title" in kwargs:
title.set_text(kwargs.pop("title"))
title_kwargs = {k: v for k, v in kwargs.items() if k.startswith("title_")}
for key, val in title_kwargs.items():
title.set(**{key[6:]: val})
kwargs.pop(key)
# Try to respect the frame visibility
kwargs.setdefault("frameon", old_legend.legendPatch.get_visible())
# Remove the old legend and create the new one
props.update(kwargs)
old_legend.remove()
new_legend = legend_func(handles, labels, loc=loc, **props)
new_legend.set_title(title.get_text(), title.get_fontproperties())
air = xr.tutorial.open_dataset("air_temperature").air
air.isel(lon=10, lat=[19, 21, 22]).plot.line(x="time", add_legend=True)
move_legend(plt.gca(), loc='center left', bbox_to_anchor=(1, 0.5))
plt.tight_layout()
plt.show()

Related

How to fix the location of nodes in networkx?

How can I fix the location of nodes in networkx? Each frame the cells move because the edges added were different. I'd like to keep all the cells in the same location.
(If you run the code snippet below on https://colab.research.google.com/ the resulting mp4 file shows how the nodes are moving around. This makes its hard to track the changes over each frame.)
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
import seaborn.apionly as sns
import matplotlib.animation
import random
G = nx.grid_2d_graph(9, 9, )
pos = {f"{x},{y}":(x,y) for x,y in G.nodes()}
ndxs = [f"{x},{y}" for x,y in G.nodes()]
G=nx.DiGraph(directed=True)
G.add_nodes_from(ndxs)
edges = [
[(f"{random.randint(0, 8)},{random.randint(0, 8)}", f"{random.randint(0, 8)},{random.randint(0, 8)}") for _ in range(10)]
for _ in range(20)
]
# Build plot
fig, ax = plt.subplots(figsize=(8,8))
def update(i):
ax.clear()
edgelist = edges[i]
nx.draw(G, pos, node_color="lightgrey", ax=ax)
nx.draw_networkx_edges(
G, pos=pos, edgelist=edgelist,
arrowstyle="->", connectionstyle=f"arc3,rad=0.5", ax=ax)
ax.set_title(f"frame {i}")
ax.set_xticks([])
ax.set_yticks([])
ani = matplotlib.animation.FuncAnimation(fig, update, frames=20, interval=250, repeat=True)
ani.save("tmp.mp4")
plt.show()

making matplotlib stacked bar chart interactive in jupyter using plotly

First time using jupyter & plotly, so I'm really struggling
This is my code
# import packages
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
import seaborn as sns
import plotly.plotly as py
import plotly.tools as tls
from plotly.graph_objs import *
py.sign_in("x", "y")
tls.set_credentials_file(username='x', api_key='y')
tls.get_credentials_file()
fig1 = plt.figure()
%matplotlib notebook
# load data
revels_data = pd.read_csv("directory.data.txt")
rd = revels_data
# grouped bar plot
grouped = rd.groupby(["Packet number", "Flavour"])["Contents"].sum().unstack().fillna(0)
grouped.plot(kind="bar", stacked=True, color=["#a2653e", "#a6814c", "#fd5956", "#fd8d49", "#9c6da5", "#c9ae74"])
# title and axis labels
plt.title("NUMBER OF REVELS PER PACKET", weight="bold")
plt.ylabel("Contents")
plt.xlabel("Packet")
# plot median line
median = 19
plt.axhline(median, color='grey', linestyle='dashed', linewidth=0.5)
# legend properties
plt.legend(loc=2, prop={"size":7})
# extend y axis to fit legend in
axes = plt.gca()
axes.set_ylim([0,30])
# show plot
plt.show()
which produces the plot:
This displays in jupyter fine.
Now, what I want to do it add hover functionality essentially exactly like this example:
https://plot.ly/matplotlib/bar-charts/#stacked-bar-chart-with-labels
where hovering the mouse over each bar shows the flavour distribution per pack.
i.e: individual tabs for:
total: a,
orange: b,
toffee: c,
etc.
appear when hovering over each packet's bar in their respective flavour's colour.
I have had a fiddle for about 3 hours but I am getting no where.
Please help, thanks!
EDIT:
# import packages
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
import seaborn as sns
import plotly.plotly as py
import plotly.tools as tls
from plotly.graph_objs import *
py.sign_in("x", "y")
tls.set_credentials_file(username='x', api_key='y')
tls.get_credentials_file()
%matplotlib notebook
# load data
revels_data = pd.read_csv(".txt")
rd = revels_data
fig1 = plt.figure()
ax = mpl_fig.add_subplot
# grouped bar plot
grouped = rd.groupby(["Packet number", "Flavour"])["Contents"].sum().unstack().fillna(0)
ax.grouped.plot(kind="bar", stacked=True, color=["#a2653e", "#a6814c", "#fd5956", "#fd8d49", "#9c6da5", "#c9ae74"])
# title and axis labels
plt.title("NUMBER OF REVELS PER PACKET", weight="bold")
plt.ylabel("Contents")
plt.xlabel("Packet")
# plot median line
median = 19
plt.axhline(median, color='grey', linestyle='dashed', linewidth=0.5)
# legend properties
plt.legend(loc=2, prop={"size":7})
# extend y axis to fit legend in
axes = plt.gca()
axes.set_ylim([0,30])
# show plot
plotly_fig = tls.mpl_to_plotly(fig1)
py.plot()
error: AttributeError: 'function' object has no attribute 'grouped'
If you already have an axes, you can provide it to the pandas plot function of a dataframe df using the ax argument,
fig, ax = plt.subplots()
df.plot(... , ax=ax)
Try this code (no guarantee that it works, since I cannot text it):
# import packages
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import plotly.offline as py
import plotly.tools as tls
from plotly.graph_objs import *
#py.sign_in("x", "y")
#tls.set_credentials_file(username='x', api_key='y')
#tls.get_credentials_file()
#%matplotlib notebook
# load data
flavours=["orange", "toffee", "chocolate", "malteser", "raisin", "coffee"]
num = np.arange(0, 6*36) % 36
flavs = np.random.choice(flavours, size=len(num))
conts = np.random.randint(0,6, len(num))
rd = pd.DataFrame({"Packet number":num ,"Flavour":flavs,"Contents" : conts})
fig1 = plt.figure(figsize=(10,8))
ax = fig1.add_subplot(111)
# grouped bar plot
grouped = rd.groupby(["Packet number", "Flavour"])["Contents"].sum().unstack().fillna(0)
grouped.plot(kind="bar", stacked=True, legend=False,
color=["#a2653e", "#a6814c", "#fd5956", "#fd8d49", "#9c6da5", "#c9ae74"], ax=ax)
#ax.plot([1,3])
# title and axis labels
plt.title("NUMBER OF REVELS PER PACKET")
plt.ylabel("Contents")
plt.xlabel("Packet")
# plot median line, not shown in plotly
median = 19
ax.axhline(median, color='grey', linestyle='dashed', linewidth=0.5)
# show plot
plotly_fig = tls.mpl_to_plotly(fig1)
# For Legend
plotly_fig["layout"]["showlegend"] = True
# change this to label the legend
#plotly_fig["data"][0]["name"] = "Men"
#plotly_fig["data"][1]["name"] = "Women"
plot_url = py.plot(plotly_fig, filename='stacked-bar-chart.html')

Embedding small plots inside subplots in matplotlib

If you want to insert a small plot inside a bigger one you can use Axes, like here.
The problem is that I don't know how to do the same inside a subplot.
I have several subplots and I would like to plot a small plot inside each subplot.
The example code would be something like this:
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
for i in range(4):
ax = fig.add_subplot(2,2,i)
ax.plot(np.arange(11),np.arange(11),'b')
#b = ax.axes([0.7,0.7,0.2,0.2])
#it gives an error, AxesSubplot is not callable
#b = plt.axes([0.7,0.7,0.2,0.2])
#plt.plot(np.arange(3),np.arange(3)+11,'g')
#it plots the small plot in the selected position of the whole figure, not inside the subplot
Any ideas?
I wrote a function very similar to plt.axes. You could use it for plotting yours sub-subplots. There is an example...
import matplotlib.pyplot as plt
import numpy as np
#def add_subplot_axes(ax,rect,facecolor='w'): # matplotlib 2.0+
def add_subplot_axes(ax,rect,axisbg='w'):
fig = plt.gcf()
box = ax.get_position()
width = box.width
height = box.height
inax_position = ax.transAxes.transform(rect[0:2])
transFigure = fig.transFigure.inverted()
infig_position = transFigure.transform(inax_position)
x = infig_position[0]
y = infig_position[1]
width *= rect[2]
height *= rect[3] # <= Typo was here
#subax = fig.add_axes([x,y,width,height],facecolor=facecolor) # matplotlib 2.0+
subax = fig.add_axes([x,y,width,height],axisbg=axisbg)
x_labelsize = subax.get_xticklabels()[0].get_size()
y_labelsize = subax.get_yticklabels()[0].get_size()
x_labelsize *= rect[2]**0.5
y_labelsize *= rect[3]**0.5
subax.xaxis.set_tick_params(labelsize=x_labelsize)
subax.yaxis.set_tick_params(labelsize=y_labelsize)
return subax
def example1():
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)
rect = [0.2,0.2,0.7,0.7]
ax1 = add_subplot_axes(ax,rect)
ax2 = add_subplot_axes(ax1,rect)
ax3 = add_subplot_axes(ax2,rect)
plt.show()
def example2():
fig = plt.figure(figsize=(10,10))
axes = []
subpos = [0.2,0.6,0.3,0.3]
x = np.linspace(-np.pi,np.pi)
for i in range(4):
axes.append(fig.add_subplot(2,2,i))
for axis in axes:
axis.set_xlim(-np.pi,np.pi)
axis.set_ylim(-1,3)
axis.plot(x,np.sin(x))
subax1 = add_subplot_axes(axis,subpos)
subax2 = add_subplot_axes(subax1,subpos)
subax1.plot(x,np.sin(x))
subax2.plot(x,np.sin(x))
if __name__ == '__main__':
example2()
plt.show()
You can now do this with matplotlibs inset_axes method (see docs):
from mpl_toolkits.axes_grid.inset_locator import inset_axes
inset_axes = inset_axes(parent_axes,
width="30%", # width = 30% of parent_bbox
height=1., # height : 1 inch
loc=3)
Update: As Kuti pointed out, for matplotlib version 2.1 or above, you should change the import statement to:
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
There is now also a full example showing all different options available.
From matplotlib 3.0 on, you can use matplotlib.axes.Axes.inset_axes:
import numpy as np
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2,2)
for ax in axes.flat:
ax.plot(np.arange(11),np.arange(11))
ins = ax.inset_axes([0.7,0.7,0.2,0.2])
plt.show()
The difference to mpl_toolkits.axes_grid.inset_locator.inset_axes mentionned in #jrieke's answer is that this is a lot easier to use (no extra imports etc.), but has the drawback of being slightly less flexible (no argument for padding or corner locations).
source: https://matplotlib.org/examples/pylab_examples/axes_demo.html
from mpl_toolkits.axes_grid.inset_locator import inset_axes
import matplotlib.pyplot as plt
import numpy as np
# create some data to use for the plot
dt = 0.001
t = np.arange(0.0, 10.0, dt)
r = np.exp(-t[:1000]/0.05) # impulse response
x = np.random.randn(len(t))
s = np.convolve(x, r)[:len(x)]*dt # colored noise
fig = plt.figure(figsize=(9, 4),facecolor='white')
ax = fig.add_subplot(121)
# the main axes is subplot(111) by default
plt.plot(t, s)
plt.axis([0, 1, 1.1*np.amin(s), 2*np.amax(s)])
plt.xlabel('time (s)')
plt.ylabel('current (nA)')
plt.title('Subplot 1: \n Gaussian colored noise')
# this is an inset axes over the main axes
inset_axes = inset_axes(ax,
width="50%", # width = 30% of parent_bbox
height=1.0, # height : 1 inch
loc=1)
n, bins, patches = plt.hist(s, 400, normed=1)
#plt.title('Probability')
plt.xticks([])
plt.yticks([])
ax = fig.add_subplot(122)
# the main axes is subplot(111) by default
plt.plot(t, s)
plt.axis([0, 1, 1.1*np.amin(s), 2*np.amax(s)])
plt.xlabel('time (s)')
plt.ylabel('current (nA)')
plt.title('Subplot 2: \n Gaussian colored noise')
plt.tight_layout()
plt.show()

matplotlib change a Patch in PatchCollection

PatchCollection accepts a list of Patches and allows me to transform / add them to a canvas all at once. But changes to the one of the Patches after the construction of the PatchCollection object are not reflected
for example:
import matplotlib.pyplot as plt
import matplotlib as mpl
rect = mpl.patches.Rectangle((0,0),1,1)
rect.set_xy((1,1))
collection = mpl.collections.PatchCollection([rect])
rect.set_xy((2,2))
ax = plt.figure(None).gca()
ax.set_xlim(0,5)
ax.set_ylim(0,5)
ax.add_artist(collection)
plt.show() #shows a rectangle at (1,1), not (2,2)
I'm looking for a matplotlib collection that will group patches just so I can transform them together, but I want to be able to change the individual patches as well.
I don't know of a collection which will do what you want, but you could write one for yourself fairly easily:
import matplotlib.collections as mcollections
import matplotlib.pyplot as plt
import matplotlib as mpl
class UpdatablePatchCollection(mcollections.PatchCollection):
def __init__(self, patches, *args, **kwargs):
self.patches = patches
mcollections.PatchCollection.__init__(self, patches, *args, **kwargs)
def get_paths(self):
self.set_paths(self.patches)
return self._paths
rect = mpl.patches.Rectangle((0,0),1,1)
rect.set_xy((1,1))
collection = UpdatablePatchCollection([rect])
rect.set_xy((2,2))
ax = plt.figure(None).gca()
ax.set_xlim(0,5)
ax.set_ylim(0,5)
ax.add_artist(collection)
plt.show() # now shows a rectangle at (2,2)

viewing a polygon read from shapefile with matplotlib

I am trying to view a basic polygon read from a Shapefile using matplotlib and pyshp
But all my efforts yield just an empty axes with no polygon. Here are few of my tries, using the dataset showing the borders of Belgium:
import shapefile as sf
r = sf.Reader("BEL_adm/BEL_adm0")
p=r.shapes()
b=p[0]
points = b.points
import matplotlib.pyplot as plt
from matplotlib.path import Path
imporst matplotlib.patches as patches
verts = points
verts = []
for x,y in points:
verts.append(tuple([x,y]))
codes = ['']*len(verts)
codes[0] = Path.MOVETO
codes[-1] = Path.CLOSEPOLY
for i in range(1,len(verts)):
codes[i]=Path.LINETO
path = Path(verts, codes)
fig = plt.figure()
ax = fig.add_subplot(111)
patch = patches.PathPatch(path, facecolor='orange', lw=2)
ax.add_patch(patch)
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)
plt.show()
Another try with patches also yields an empty frame:
fig = plt.figure(figsize=(11.7,8.3))
ax = plt.subplot(111)
x,y=zip(*b.points)
import matplotlib.patches as patches
import matplotlib.pyplot as plt
bol=patches.Polygon(b.points,True, transform=ax.transAxes)
ax.add_patch(bol)
ax.set_ylim(0,60)
ax.set_xlim(0,200)
plt.show()
Would be happy to see what I am missing.
Thanks, Oz
instead of calling set_xlim(), set_ylim() to set the range of axis, you can use ax.autoscale().
For your Polygon version, you don't need to set transform argument to ax.transAxes, just call:
bol=patches.Polygon(b.points,True)