Legend in separate subplot and grid - python-3.x

I have a collection of plots, arranged in two grids. In the left grid, I have one plot in the top (whole width) and two in the bottom (side-by-side). The two in the bottom are sharing legends. In my right grid, I want the legends, it is a lots of data series, and I would like to use the whole height of my figure.
The appearance of the data series are animated, but I would like the legends not to be.
My idea was to draw the time series in my right grid with legends, and hide the data series. But my only solution is ax.set_visible(False), which removes everything.
This is principally how the script looks like (simplified version):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.animation as anim
from matplotlib.gridspec import GridSpec
data = np.array([[1],[2],[3],[4]])
sett = np.array([1,2,3,4])
data1 = np.hstack((data,data*2, data*3, data*4))
data2 = np.hstack((3*data, 3*data/2, 3*data/3, 3*data/4))
df1 = pd.DataFrame(data = np.array(data1), index = [1,2,3,4], columns =
sett).transpose()
df2 = pd.DataFrame(data = np.array(data2), index = [1,2,3,4], columns =
sett).transpose()
gs1 = GridSpec(2,2)
gs1.update(left=0.05, right = 0.80, hspace = 0.05)
gs2 = GridSpec(3,1)
gs2.update(left=0.85, right = 0.98, hspace = 0.05)
figure = plt.figure()
plt.clf()
ax1 = plt.subplot(gs1[0,:])
ax2 = plt.subplot(gs1[1,0])
ax3 = plt.subplot(gs1[1,1], sharey = ax2)
ax4 = plt.subplot(gs2[:,0])
ax1.set_ylim(0,25)
label = ['s0', 's1', 's2', 's3', 's4']
ax4.plot(df1[1], df2[:])
ax4.legend(labels = label)
def make_frame(i):
ct=sett[i]
ax2.plot(df1[1], df1[ct])
ax3.plot(df1[1], df2[ct])
ax3.legend(labels = label)
ani = anim.FuncAnimation(figure, make_frame, frames = len(sett),
interval =500, repeat = False)
plt.show()
How can I remove the data series and keep the legend in gs2/ax4?
Don't bother I plot the first data series twice in ax2 and ax3 - it is ok in my original script. However - if someone can enlighten me on why, it is very much appreciated.

I'm not entirely sure what the desired output should be. Are you trying to put the legend at the place of ax4 right now, but not have the plot shown in ax4 at the moment.
My solution would be to not create ax4 at all. Instead you can use bbox_to_anchor to move the position of the legend. Here I use the transform from ax1 to establish a location in reference to ax1 and I move the legend slightly past the right edge and at the top of ax1.
See "legend guide" for more information.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.animation as anim
from matplotlib.gridspec import GridSpec
data = np.array([[1], [2], [3], [4]])
sett = np.array([1, 2, 3, 4])
data1 = np.hstack((data, data * 2, data * 3, data * 4))
data2 = np.hstack((3 * data, 3 * data / 2, 3 * data / 3, 3 * data / 4))
df1 = pd.DataFrame(data=np.array(data1), index=[1, 2, 3, 4], columns=sett).transpose()
df2 = pd.DataFrame(data=np.array(data2), index=[1, 2, 3, 4], columns=sett).transpose()
gs1 = GridSpec(2, 2)
gs1.update(left=0.05, right=0.80, hspace=0.05)
figure = plt.figure()
plt.clf()
ax1 = plt.subplot(gs1[0, :])
ax2 = plt.subplot(gs1[1, 0])
ax3 = plt.subplot(gs1[1, 1], sharey=ax2)
label = ['s0', 's1', 's2', 's3', 's4']
def make_frame(i):
ct = sett[i]
ax2.plot(df1[1], df1[ct])
ax3.plot(df1[1], df2[ct])
ax3.legend(labels=label, loc='upper left', bbox_to_anchor=(1.05, 1.), bbox_transform=ax1.transAxes)
ani = anim.FuncAnimation(figure, make_frame, frames=len(sett),
interval=500, repeat=False)
plt.show()
EDIT: using a proxy artist to create all the legends before the animation starts
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.animation as anim
from matplotlib.gridspec import GridSpec
data = np.array([[1], [2], [3], [4]])
sett = np.array([1, 2, 3, 4])
data1 = np.hstack((data, data * 2, data * 3, data * 4))
data2 = np.hstack((3 * data, 3 * data / 2, 3 * data / 3, 3 * data / 4))
df1 = pd.DataFrame(data=np.array(data1), index=[1, 2, 3, 4], columns=sett).transpose()
df2 = pd.DataFrame(data=np.array(data2), index=[1, 2, 3, 4], columns=sett).transpose()
gs1 = GridSpec(2, 2)
gs1.update(left=0.05, right=0.80, hspace=0.05)
figure = plt.figure()
plt.clf()
ax1 = plt.subplot(gs1[0, :])
ax2 = plt.subplot(gs1[1, 0])
ax3 = plt.subplot(gs1[1, 1], sharey=ax2)
ax1.set_ylim(0, 25)
labels = ['s0', 's1', 's2', 's3', 's4']
colors = ['C0', 'C1', 'C2', 'C3', 'C4']
proxies = [plt.plot([], [], c=c)[0] for c in colors]
ax1.legend(proxies, labels, bbox_to_anchor=(1., 1.), loc="upper left")
def init_frame():
pass
def make_frame(i):
ct = sett[i]
ax2.plot(df1[1], df1[ct], c=colors[i], label=labels[i])
ax3.plot(df1[1], df2[ct], c=colors[i], label=labels[i])
ax3.legend()
ani = anim.FuncAnimation(figure, make_frame, init_func=init_frame, frames=len(sett),
interval=500, repeat=False)
plt.show()

I would create the line plots prior to animating anything. You can initialize them with empty lists and then set the data one by one.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.animation as anim
from matplotlib.gridspec import GridSpec
data = np.array([1,2,3,4,5])
data1 = np.vstack((data,data*2, data*3, data*4))
data2 = np.vstack((3*data, 3*data/2, 3*data/3, 3*data/4))
df1 = pd.DataFrame(data1)
df2 = pd.DataFrame(data2)
sett = np.arange(len(df1.columns))
gs1 = GridSpec(2,2)
gs1.update(left=0.05, right = 0.80, hspace = 0.05)
figure = plt.figure()
ax1 = plt.subplot(gs1[0,:])
ax2 = plt.subplot(gs1[1,0])
ax3 = plt.subplot(gs1[1,1], sharey = ax2, sharex= ax2)
ax2.set_ylim(0,25)
lines1 = ax2.plot(*[[] for _ in range(len(sett)*2)])
lines2 = ax3.plot(*[[] for _ in range(len(sett)*2)])
label = ['s0', 's1', 's2', 's3', 's4']
ax1.legend(handles = lines1, labels=label, bbox_to_anchor=(1.05,1), loc="upper left")
def init():
for line in lines1+lines2:
line.set_data([],[])
def make_frame(i):
ct=sett[i]
lines1[i].set_data(df1.index, df1[ct])
lines2[i].set_data(df1.index, df2[ct])
ax2.relim()
ax2.autoscale_view()
ani = anim.FuncAnimation(figure, make_frame, init_func=init, frames = len(sett),
interval =500, repeat = False)
ani.save("anigif.gif", writer="imagemagick")
plt.show()

Related

Matplotlib: how to get color bars that are one on top of each other as opposed to side by side?

I have the following code:
import matplotlib.pyplot as plt
import numpy as np
img1 = np.zeros([512,512])
img2 = np.zeros([512,512])
plt.figure(figsize=(10,10))
plt.imshow(img1, cmap='inferno')
plt.axis('off')
cba = plt.colorbar(shrink=0.25)
cba.ax.set_ylabel('Events / counts', fontsize=14)
cba.ax.tick_params(labelsize=12)
plt.imshow(img2, cmap='turbo', alpha=0.5)
plt.axis('off')
cba = plt.colorbar(shrink=0.25)
cba.ax.set_ylabel('Lifetime / ns)', fontsize=14)
cba.ax.tick_params(labelsize=12)
plt.tight_layout()
plt.show()
which produces the following output:
My question is, how can I get color bars that are on top of one another as opposed to next to each other? Ideally, I would like to get something like this:
You can grab the position of the ax and use it to create new axes for the colorbars. Here is an example:
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage
data = ndimage.gaussian_filter(np.random.randn(512, 512), sigma=15, mode='nearest') * 20
fig, ax = plt.subplots()
im1 = ax.imshow(data, vmin=-1, vmax=0, cmap='viridis')
data[data < 0] = np.nan
im2 = ax.imshow(data, vmin=0.001, vmax=1, cmap='Reds_r')
ax.axis('off')
pos = ax.get_position()
bar_h = (pos.y1 - pos.y0) * 0.5 # 0.5 joins the two bars, e.g. 0.48 separates them a bit
ax_cbar1 = fig.add_axes([pos.x1 + 0.02, pos.y0, 0.03, bar_h])
cbar1 = fig.colorbar(im1, cax=ax_cbar1, orientation='vertical')
ax_cbar2 = fig.add_axes([pos.x1 + 0.02, pos.y1 - bar_h, 0.03, bar_h])
cbar2 = fig.colorbar(im2, cax=ax_cbar2, orientation='vertical')
plt.show()

Why does not this plot get bigger as expected?

From a previous question, I got that plt.figure(figsize = 2 * np.array(plt.rcParams['figure.figsize'])) will increase the plot size by 2 times. With below code, I want to plot 4 subplots in the grid 2x2.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
%config InlineBackend.figure_format = 'svg' # Change the image format to svg for better quality
don = pd.read_csv('https://raw.githubusercontent.com/leanhdung1994/Deep-Learning/main/donclassif.txt.gz', sep=';')
fig, ax = plt.subplots(nrows = 2, ncols = 2)
plt.figure(figsize = 2 * np.array(plt.rcParams['figure.figsize'])) # This is to have bigger plot
for row in ax:
for col in row:
kmeans = KMeans(n_clusters = 4)
kmeans.fit(don)
y_kmeans = kmeans.predict(don)
col.scatter(don['V1'], don['V2'], c = y_kmeans, cmap = 'viridis')
centers = kmeans.cluster_centers_
col.scatter(centers[:, 0], centers[:, 1], c = 'red', s = 200, alpha = 0.5);
plt.show()
Could you please explain why plt.figure(figsize = 2 * np.array(plt.rcParams['figure.figsize'])) does not work in this case?
I post #JohanC's comment to remove this question from unanswered list.
It could be written as fig, axes = plt.subplots(nrows=2, ncols=2, figsize=2 * np.array(plt.rcParams['figure.figsize'])). Just calling plt.figure without storing the result creates a dummy new figure, without changing fig and without creating the axes on that new figure won't have the desired result.

Highlight a point in grouped boxplot in seaborn

I need to highlight a specific point in each boxplot. For example, I want to highlight the point where petal_width is 0.8 in a boxplot chart for petal_length for each species.
Here is the example:
iris = sns.load_dataset('iris')
##Create three points where petal_width is 0.8 for each species
iris_2 = pd.DataFrame({'sepal_length':Series([1,2,3],dtype='float32'), 'sepal_width':Series([1.1,2.1,3.1],dtype='float32'),
'petal_length':Series([1,2,3],dtype='float32'), 'petal_width':Series([0.8,0.8,0.8],dtype='float32'),
'species':Series(['setosa','versicolor','virginica'])})
iris_all = pd.concat([iris, iris_2]).reset_index(drop = True)
sns.boxplot(x='species', y = 'petal_length', data = iris_all)
sns.regplot(x= iris_all['species'][iris_all['petal_width'] == 0.8],
y= iris_all['petal_length'][iris_all['petal_width'] == 0.8], scatter=True, fit_reg=False, marker='o',
scatter_kws={"s": 100})
But the code doesn't work. I wonder how I can correct it. Thanks.
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
iris = sns.load_dataset('iris')
# Create three points where petal_width is 0.8 for each species
iris_2 = pd.DataFrame(
{'sepal_length': pd.Series([1, 2, 3], dtype='float32'), 'sepal_width': pd.Series([1.1, 2.1, 3.1], dtype='float32'),
'petal_length': pd.Series([1, 2, 3], dtype='float32'), 'petal_width': pd.Series([0.8, 0.8, 0.8], dtype='float32'),
'species': pd.Series(['setosa', 'versicolor', 'virginica'])})
iris_all = pd.concat([iris, iris_2]).reset_index(drop=True)
sns.boxplot(x='species', y='petal_length', data=iris_all)
sns.regplot(x=iris_all['species'][(iris_all['petal_width'] > 0.79) & (iris_all['petal_width'] < 0.81)],
y=iris_all['petal_length'][(iris_all['petal_width'] > 0.79) & (iris_all['petal_width'] < 0.81)],
color='blue',
scatter=True, fit_reg=False,
marker='+',
scatter_kws={"s": 100})
plt.show()

How to plot a line representing a value from a dataframe with two geometry columns?

I have the following data:
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
import matplotlib.pyplot as plt
points = gpd.GeoDataFrame([['A', Point(1.5, 1.75), Point(2, 2), 16],
['B', Point(3.0,2.0), Point(3, 4), 18],
['C', Point(2.5,1.25), Point(1, 1), 19]],
columns=['id', 'geometry', 'geometry b', 'value'],
geometry='geometry')
the first geometry column represents the starting point, the second the ending point of a line which has a value corresponding to the value column.
I have tried to plot this with:
f, ax = plt.subplots(1, figsize = [12, 12])
points.plot(ax=ax, column = 'value')
However it just plots the first geometry column and colours the points corresponding to their value.
How do I produce a plot that draws the lines, colour coded to their value?
This working code and its output plot demonstrate how you can achieve what you need. See comments in the code for more details.
import geopandas as gpd
from shapely.geometry import Point, LineString
import matplotlib.pyplot as plt
# handle all points and create relating lines
pA1 = Point(1.5, 1.75)
pA2 = Point(2, 2)
line_A = LineString([[pA1.x, pA1.y], [pA2.x, pA2.y]])
pB1 = Point(3.0, 2.0)
pB2 = Point(3, 4)
line_B = LineString([[pB1.x, pB1.y], [pB2.x, pB2.y]])
pC1 = Point(2.5, 1.25)
pC2 = Point(1, 1)
line_C = LineString([[pC1.x, pC1.y], [pC2.x, pC2.y]])
# create a geodataframe,
# assigning the column containing `LineString` as its geometry
pts_and_lines = gpd.GeoDataFrame([['A', pA1, pA2, 16, line_A],
['B', pB1, pB2, 18, line_B],
['C', pC1, pC2, 19, line_C]],
columns=['id', 'beg_pt', 'end_pt', 'value', 'LineString_obj'],
geometry='LineString_obj') # declare LineString (last column) as the `geometry`
# make a plot of the geodataframe obtained
f, ax = plt.subplots(1, figsize = [4, 4])
pts_and_lines.plot(ax=ax, column = 'value');
plt.show()
The output plot:
If you prefer to build a dataframe containing from_point and to_point first, then append new column containing LineString creating from the existing points, here is an alternative code.
import geopandas as gpd
from shapely.geometry import Point, LineString
import matplotlib.pyplot as plt
# this dataframe `points_df` contains from_point, to_point for creating `lineString`.
points_df = gpd.GeoDataFrame([['A', Point(1.5, 1.75), Point(2, 2), 16],
['B', Point(3.0,2.0), Point(3, 4), 18],
['C', Point(2.5,1.25), Point(1, 1), 19]],
columns=['id', 'geometry_a', 'geometry_b', 'value'])
# add new column, `line` to the dataframe,
# this column contains `LineString` geometry.
points_df['line'] = points_df.apply(lambda x: LineString([x['geometry_a'], x['geometry_b']]), axis=1)
# assign geometry to `points_df` using the column that has `LineString` geometry
# take the result as `target_gdf`
# `target_gdf` is now capable of plotting with matplotlib
target_gdf = gpd.GeoDataFrame(points_df, geometry=points_df['line'])
f, ax = plt.subplots(1, figsize = [4, 4])
target_gdf.plot(ax=ax, column = 'value');
plt.show()
Its output plot is the same as the previous one.

How to plot vertical lines in plotly offline?

How would one plot a vertical line in plotly offline, using python? I want to add lines at x=20, x=40, and x=60, all in the same plot.
def graph_contracts(self):
trace1 = go.Scatter(
x=np.array(range(len(all_prices))),
y=np.array(all_prices), mode='markers', marker=dict(size=10, color='rgba(152, 0, 0, .8)'))
data = [trace1]
layout = go.Layout(title='Market Contracts by Period',
xaxis=dict(title='Contract #',
titlefont=dict(family='Courier New, monospace', size=18, color='#7f7f7f')),
yaxis=dict(title='Prices ($)',
titlefont=dict(family='Courier New, monospace', size=18, color='#7f7f7f')))
fig = go.Figure(data=data, layout=layout)
py.offline.plot(fig)
You can add lines via shape in layout, e.g.
import plotly
plotly.offline.init_notebook_mode()
import random
x=[i for i in range(100)]
trace = plotly.graph_objs.Scatter(x=x,
y=[random.random() for _ in x],
mode='markers')
shapes = list()
for i in (20, 40, 60):
shapes.append({'type': 'line',
'xref': 'x',
'yref': 'y',
'x0': i,
'y0': 0,
'x1': i,
'y1': 1})
layout = plotly.graph_objs.Layout(shapes=shapes)
fig = plotly.graph_objs.Figure(data=[trace],
layout=layout)
plotly.offline.plot(fig)
would give you
This is my example. The most important instruction is this.
fig.add_trace(go.Scatter(x=[12, 12], y=[-300,300], mode="lines", name="SIGNAL"))
The most important attribute is MODE='LINES'.
Actually this example is about a segment with x=12
EXAMPLE
import pandas as pd
import plotly.graph_objects as go
import matplotlib.pyplot as plt
import numpy as np
import plotly.tools as tls
df1 = pd.read_csv('./jnjw_f8.csv')
layout = go.Layout(
xaxis = go.layout.XAxis(
tickmode = 'linear',
tick0 = 1,
dtick = 3
),
yaxis = go.layout.YAxis(
tickmode = 'linear',
tick0 = -100,
dtick = 3
))
fig = go.Figure(layout = layout)
fig.add_trace(go.Scatter(x = df1['x'], y =
df1['y1'],name='JNJW_sqrt'))
fig.add_trace(go.Scatter(x=[12, 12], y=[-300,300],
mode="lines", name="SIGNAL"))
fig.show()
Look here too.
how to plot a vertical line with plotly
A feature for vertical and horizontal lines is implemented with Plotly.py 4.12 (released 11/20). It works for plotly express and graph objects. See here: https://community.plotly.com/t/announcing-plotly-py-4-12-horizontal-and-vertical-lines-and-rectangles/46783
Simple example:
import plotly.express as px
df = px.data.stocks(indexed=True)
fig = px.line(df)
fig.add_vline(x='2018-09-24')
fig.show()
fig.add_vline(x=2.5, line_width=3, line_dash="dash", line_color="green")

Resources