Scope in Python subplot similar to MATLAB's stackedplot() - python

Is there a plot function available in Python that is same as MATLAB's stackedplot()?
stackedplot() in MATLAB can line plot several variables with the same X axis and are stacked vertically. Additionally, there is a scope in this plot that shows the value of all variables for a given X just by moving the cursor (please see the attached plot). I have been able to generate stacked subplots in Python with no issues, however, not able to add a scope like this that shows the value of all variables by moving the cursor. Is this feature available in Python?
This is a plot using MATLAB's stackedplot():
import pandas as pd
import numpy as np
from datetime import datetime, date, time
import matplotlib.pyplot as plt
import matplotlib
import matplotlib.transforms as transforms
import mplcursors
from collections import Counter
import collections
def flatten(x):
result = []
for el in x:
if isinstance(x, collections.Iterable) and not isinstance(el, str):
result.extend(flatten(el))
else:
result.append(el)
return result
def shared_scope(sel):
sel.annotation.set_visible(False) # hide the default annotation created by mplcursors
x = sel.target[0]
for ax in axes:
for plot in plotStore:
da = plot.get_ydata()
if type(da[0]) is np.datetime64: #pd.Timestamp
yData = matplotlib.dates.date2num(da) # to numerical values
vals = np.interp(x, plot.get_xdata(), yData)
dates = matplotlib.dates.num2date(vals) # to matplotlib dates
y = datetime.strftime(dates,'%Y-%m-%d %H:%M:%S') # to strings
annot = ax.annotate(f'{y:.30s}', (x, vals), xytext=(15, 10), textcoords='offset points',
bbox=dict(facecolor='tomato', edgecolor='black', boxstyle='round', alpha=0.5))
sel.extras.append(annot)
else:
y = np.interp(x, plot.get_xdata(), plot.get_ydata())
annot = ax.annotate(f'{y:.2f}', (x, y), xytext=(15, 10), textcoords='offset points', arrowprops=dict(arrowstyle="->",connectionstyle="angle,angleA=0,angleB=90,rad=10"),
bbox=dict(facecolor='tomato', edgecolor='black', boxstyle='round', alpha=0.5))
sel.extras.append(annot)
vline = ax.axvline(x, color='k', ls=':')
sel.extras.append(vline)
trans = transforms.blended_transform_factory(axes[0].transData, axes[0].transAxes)
text1 = axes[0].text(x, 1.01, f'{x:.2f}', ha='center', va='bottom', color='blue', clip_on=False, transform=trans)
sel.extras.append(text1)
# Data to plot
data = pd.DataFrame(columns = ['timeOfSample','Var1','Var2'])
data.timeOfSample = ['2020-05-10 09:09:02','2020-05-10 09:09:39','2020-05-10 09:40:07','2020-05-10 09:40:45','2020-05-12 09:50:45']
data['timeOfSample'] = pd.to_datetime(data['timeOfSample'])
data.Var1 = [10,50,100,5,25]
data.Var2 = [20,55,70,60,50]
variables = ['timeOfSample',['Var1','Var2']] # variables to plot - Var1 and Var2 to share a plot
nPlot = len(variables)
dataPts = np.arange(0, len(data[variables[0]]), 1) # x values for plots
plotStore = [0]*len(flatten(variables)) # to store all the plots for annotation purposes later
fig, axes = plt.subplots(nPlot,1,sharex=True)
k=0
for i in range(nPlot):
if np.size(variables[i])==1:
yData = data[variables[i]]
line, = axes[i].plot(dataPts,yData,label = variables[i])
plotStore[k]=line
k = k+1
else:
for j in range(np.size(variables[i])):
yData = data[variables[i][j]]
line, = axes[i].plot(dataPts,yData,label = variables[i][j])
plotStore[k]=line
k = k+1
axes[i].set_ylabel(variables[i])
cursor = mplcursors.cursor(plotStore, hover=True)
cursor.connect('add', shared_scope)
plt.xlabel('Samples')
plt.show()

mplcursors can be used to create annotations while hovering, moving texts and vertical bars. sel.extras.append(...) helps to automatically hide the elements that aren't needed anymore.
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
import mplcursors
import numpy as np
def shared_scope(sel):
x = sel.target[0]
annotation_text = f'x: {x:.2f}'
for ax, plot in zip(axes, all_plots):
y = np.interp(x, plot.get_xdata(), plot.get_ydata())
annotation_text += f'\n{plot.get_label()}: {y:.2f}'
vline = ax.axvline(x, color='k', ls=':')
sel.extras.append(vline)
sel.annotation.set_text(annotation_text)
trans = transforms.blended_transform_factory(axes[0].transData, axes[0].transAxes)
text1 = axes[0].text(x, 1.01, f'{x:.2f}', ha='center', va='bottom', color='blue', clip_on=False, transform=trans)
sel.extras.append(text1)
fig, axes = plt.subplots(figsize=(15, 10), nrows=3, sharex=True)
y1 = np.random.uniform(-1, 1, 100).cumsum()
y2 = np.random.uniform(-1, 1, 100).cumsum()
y3 = np.random.uniform(-1, 1, 100).cumsum()
all_y = [y1, y2, y3]
all_labels = ['Var1', 'Var2', 'Var3']
all_plots = [ax.plot(y, label=label)[0]
for ax, y, label in zip(axes, all_y, all_labels)]
for ax, label in zip(axes, all_labels):
ax.set_ylabel(label)
cursor = mplcursors.cursor(all_plots, hover=True)
cursor.connect('add', shared_scope)
plt.show()
Here is a version with separate annotations per subplot:
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
import mplcursors
import numpy as np
def shared_scope(sel):
sel.annotation.set_visible(False) # hide the default annotation created by mplcursors
x = sel.target[0]
for ax, plot in zip(axes, all_plots):
y = np.interp(x, plot.get_xdata(), plot.get_ydata())
vline = ax.axvline(x, color='k', ls=':')
sel.extras.append(vline)
annot = ax.annotate(f'{y:.2f}', (x, y), xytext=(5, 0), textcoords='offset points',
bbox=dict(facecolor='tomato', edgecolor='black', boxstyle='round', alpha=0.5))
sel.extras.append(annot)
trans = transforms.blended_transform_factory(axes[0].transData, axes[0].transAxes)
text1 = axes[0].text(x, 1.01, f'{x:.2f}', ha='center', va='bottom', color='blue', clip_on=False, transform=trans)
sel.extras.append(text1)
fig, axes = plt.subplots(figsize=(15, 10), nrows=3, sharex=True)
y1 = np.random.uniform(-1, 1, 100).cumsum()
y2 = np.random.uniform(-1, 1, 100).cumsum()
y3 = np.random.uniform(-1, 1, 100).cumsum()
all_y = [y1, y2, y3]
all_labels = ['Var1', 'Var2', 'Var3']
all_plots = [ax.plot(y, label=label)[0]
for ax, y, label in zip(axes, all_y, all_labels)]
for ax, label in zip(axes, all_labels):
ax.set_ylabel(label)
cursor = mplcursors.cursor(all_plots, hover=True)
cursor.connect('add', shared_scope)
plt.show()

Related

Gradient 2D plot using contourf

I did a test code brigging something I saw on stack on different topic, and try to assemble it to make what I need : a filled curve with gradient.
After validate this test code I will make a subplot (4 plots for 4 weeks) with the same min/max for all plot (it's a power consumption).
My code :
from matplotlib import pyplot as plt
import numpy as np
# random x
x = range(100)
# smooth random y
y = 0
result = []
for _ in x:
result.append(y)
y += np.random.normal(loc=0, scale=1)#, size=len(x))
y = result
y = list(map(abs, y))
# creation of z for contour
z1 = min(y)
z3 = max(y)/(len(x)+1)
z2 = max(y)-z3
z = [[z] * len(x) for z in np.arange(z1,z2,z3)]
num_bars = len(x) # more bars = smoother gradient
# plt.contourf(x, y, z, num_bars, cmap='greys')
plt.contourf(x, y, z, num_bars, cmap='cool', levels=101)
background_color = 'w'
plt.fill_between(
x,
y,
y2=max(y),
color=background_color
)
But everytime I make the code run, the result display a different gradient scale, that is not smooth neither even straight right.
AND sometime the code is in error : TypeError: Length of y (100) must match number of rows in z (101)
I'm on it since too many time, turning around, and can't figure where I'm wrong...
I finally find something particularly cool, how to :
have both filled gradient curves in a different color (thanks to JohanC in this topic)
use x axis with datetime (thanks to Ffisegydd in this topic)
Here the code :
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import matplotlib.dates as mdates
np.random.seed(2022)
st_date = '2022-11-01 00:00:00'
st_date = pd.to_datetime(st_date)
en_date = st_date + pd.DateOffset(days=7)
x = pd.date_range(start=st_date,end=en_date,freq='30min')
x = mdates.date2num(x)
y = np.random.normal(0.01, 1, len(x)).cumsum()
fig, ax = plt.subplots(figsize=(18, 5))
ax.plot(x, y, color='grey')
########################
# positives fill
#######################
grad1 = ax.imshow(
np.linspace(0, 1, 256).reshape(-1, 1),
cmap='Blues',
vmin=-0.5,
aspect='auto',
extent=[x.min(), x.max(), 0, y.max()],
# extent=[x[0], x[1], 0, y.max()],
origin='lower'
)
poly_pos = ax.fill_between(x, y.min(), y, alpha=0.1)
grad1.set_clip_path(
poly_pos.get_paths()[0],
transform=ax.transData
)
poly_pos.remove()
########################
# negatives fill
#######################
grad2 = ax.imshow(
np.linspace(0, 1, 256).reshape(-1, 1),
cmap='Reds',
vmin=-0.5,
aspect='auto',
extent=[x.min(), x.max(), y.min(), 0],
origin='upper'
)
poly_neg = ax.fill_between(x, y, y.max(), alpha=0.1)
grad2.set_clip_path(
poly_neg.get_paths()[0],
transform=ax.transData
)
poly_neg.remove()
########################
# decorations and formatting plot
########################
ax.xaxis_date()
date_format = mdates.DateFormatter('%d-%b %H:%M')
ax.xaxis.set_major_formatter(date_format)
fig.autofmt_xdate()
ax.grid(True)

Picker Event to display legend labels in matplotlib Ver. 3

Continue on from previous question .. Here
This time I need to plot two sets of data with different sizes on the same plot. The issue is, since the two sets of data have different sizes, some points will fall out of index. When I pick on certain point(s), I'll get a warning like this:
File "C:\ProgramData\Anaconda3\lib\site-packages\matplotlib\cbook\__init__.py", line 224, in process
func(*args, **kwargs)
File "C:\Users\U240335\.spyder-py3\Spyder_Dingyi\untitled0.py", line 42, in onpick
IndexError: index 16 is out of bounds for axis 0 with size 10
Traceback (most recent call last):
File "C:\ProgramData\Anaconda3\lib\site-packages\matplotlib\cbook\__init__.py", line 224, in process
func(*args, **kwargs)
File "C:\Users\U240335\.spyder-py3\Spyder_Dingyi\untitled0.py", line 42, in onpick
IndexError: index 17 is out of bounds for axis 0 with size 10
And I would also like to show the value of the point (including the label) on the plot of which the cursor is clicking on and be able to do something like "clear all" after marking some points on the plot.
Here's the full code:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
a = np.random.uniform(0, 20, 30)
b = np.random.uniform(0, 20, 30)
x = np.random.uniform(0, 20, 30)
y = np.random.uniform(0, 20, 30)
z = [0]*10 + [1]*20
random.shuffle(z)
# x y data and legend labels
df1 = pd.DataFrame({"x1": a, "y1": b, 'z': z})
df2 = pd.DataFrame({"x2": x, "y2": y, 'z': z})
x1 = df1['x1'].loc[df1['z']==0].reset_index(drop=True).values
y1 = df1['y1'].loc[df1['z']==0].reset_index(drop=True).values
x2 = df2['x2'].loc[df1['z']==1].reset_index(drop=True).values
y2 = df2['y2'].loc[df1['z']==1].reset_index(drop=True).values
def overlay_plot2(x1,y1,x2,y2,title,xlabel,ylabel):
# define the picker event
def onpick(event):
ind = event.ind
print('\n%s:' % xlabel, x1[ind] if event.artist.get_label() == '0' else x2[ind], # use event.artist.getlabel() when labels are made
'%s:' % ylabel, y1[ind] if event.artist.get_label() == '1' else y2[ind],
event.artist.get_label())
# plot
fig, ax = plt.subplots(figsize=(8, 6), dpi = 120)
ax.scatter(x1, y1, c='b', label = '0',s=14, marker='x', picker=True)
ax.scatter(x2, y2, c='r', label = '1',s=14, marker='o', picker=True)
ax.set_title(title)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.legend(
loc="center left",
bbox_to_anchor=(1, 0.5),
)
ax.ticklabel_format(useOffset=False)
ax.tick_params(axis = 'x',labelrotation = 45)
plt.tight_layout()
# call the event
fig.canvas.mpl_connect('pick_event', onpick)
overlay_plot2(x1,y1,x2,y2,"title","xlabel","ylabel")
Before going too into the weeds with customizing picker events, have you taken a look at mplcursors? There is a fantastic on hover feature that may be more of what you need. You can simply do mplcursors.cursor(hover=True) to give a basic annotation with x, y, and label values. Or, you can customize the annotations. Here is some code that I use for one of my projects where I've customized everything (color, text, arrow, etc.). Maybe you can find some use for it as well?
Do at least look at my comment about how to correct your index error. If the below isn't what you want, give this answer a read and it should point you in the right direction on how to annotate on click.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
import mplcursors
a = np.random.uniform(0, 20, 30)
b = np.random.uniform(0, 20, 30)
x = np.random.uniform(0, 20, 30)
y = np.random.uniform(0, 20, 30)
z = [0]*10 + [1]*20
random.shuffle(z)
# x y data and legend labels
df1 = pd.DataFrame({"x1": a, "y1": b, 'z': z})
df2 = pd.DataFrame({"x2": x, "y2": y, 'z': z})
x1 = df1['x1'].loc[df1['z']==0].reset_index(drop=True).values
y1 = df1['y1'].loc[df1['z']==0].reset_index(drop=True).values
x2 = df2['x2'].loc[df1['z']==1].reset_index(drop=True).values
y2 = df2['y2'].loc[df1['z']==1].reset_index(drop=True).values
def overlay_plot2(x1,y1,x2,y2,title,xlabel,ylabel):
# define the picker event
def onpick(event):
ind = event.ind
print('\n%s:' % xlabel, x1[ind] if event.artist.get_label() == '0' else x2[ind], # use event.artist.getlabel() when labels are made
'%s:' % ylabel, y1[ind] if event.artist.get_label() == '0' else y2[ind],
event.artist.get_label())
# plot
fig, ax = plt.subplots(figsize=(8, 6), dpi = 120)
ax1 = ax.scatter(x1, y1, c='b', label = '0',s=14, marker='x', picker=True)
ax2 = ax.scatter(x2, y2, c='r', label = '1',s=14, marker='o', picker=True)
#mplcursors.cursor(hover=True)
#mplcursors.cursor(ax1, hover=2).connect("add")
def _(sel):
sel.annotation.set_text('X Value: {}\nY Value: {}\nLabel: {}'.format(round(sel.target[0],2), round(sel.target[1], 2), sel.artist.get_label()))
sel.annotation.get_bbox_patch().set(fc="blue", alpha=0.5)
sel.annotation.arrow_patch.set(arrowstyle="-|>", connectionstyle="angle3", fc="black", alpha=.5)
#mplcursors.cursor(ax2, hover=2).connect("add")
def _(sel):
sel.annotation.set_text('X Value: {}\nY Value: {}\nLabel: {}'.format(round(sel.target[0],2), round(sel.target[1], 2), sel.artist.get_label()))
sel.annotation.get_bbox_patch().set(fc="red", alpha=0.5)
sel.annotation.arrow_patch.set(arrowstyle="-|>", connectionstyle="angle3", fc="black", alpha=.5)
ax.set_title(title)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.legend(
loc="center left",
bbox_to_anchor=(1, 0.5),
)
ax.ticklabel_format(useOffset=False)
ax.tick_params(axis = 'x',labelrotation = 45)
plt.tight_layout()
# call the event
fig.canvas.mpl_connect('pick_event', onpick)
plt.show()
overlay_plot2(x1,y1,x2,y2,"title","xlabel","ylabel")
Example Output Graphs (my mouse is hovering over these points):

How to plot profiles in front of a 2D field in matplotlib?

I have the following script that plots me a 2D field of some quantity in the domain:
def field(x, y, z):
fig, (ax) = plt.subplots()
ax.tricontour(x, y, z)
cntr = ax.tricontourf(x, y, z)
fig.colorbar(cntr, ax=ax)
plt.show()
Which gives me something like:
I want to add profiles on top of that figure, i.e.:
How do I do this?
Just plot on ax, setting linewidth (short lw) and color (short c) as needed:
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(1)
x = np.random.uniform(-5, 40, 50)
y = np.random.uniform(-.25, .25, 50)
z = y * 100
fig, ax = plt.subplots(figsize=(10,2))
cntr = ax.tricontourf(x, y, z)
fig.colorbar(cntr, ax=ax)
ax.set(xlim=(0, 35), ylim=(-.2, .2))
for i in range(5, 35, 5):
ax.plot([i-1, i+1], [0.2, -0.2], lw=5, c='k')

Hist wrong binwidth with logarithmix x and y axis

I need to plot a hist with bot logarithmic y and x-axis, but I'd like also to have hist's bins displayed of same size.
How can I achieve this result with the following code (the x used is very long so I have intentionally avoided to insert it):
import matplotlib as plt
import numpy as np
fig, ax1 = plt.subplots()
hist, bins, _ = ax1.hist(x, log=True, color="red", rwidth=0.5)
plt.xscale("log")
np_x = np.array(x)
print("np_x.mean() = " + str(np_x.mean()))
plt.axvline(np_x.mean() * 1.1, color='lime', linestyle='dashed', linewidth=3,
label='Mean: {:.2f}'.format(np_x.mean()))
handles, labels = ax1.get_legend_handles_labels()
binwidth = math.floor(bins[1] - bins[0])
mylabel = "Binwidth: {}".format(binwidth) + ", Bins: {}".format(len(hist))
red_patch = mpatches.Patch(color='red', label=mylabel)
handles = [red_patch] + handles
labels = [mylabel] + labels
ax1.legend(handles, labels)
plt.xlabel(x_label)
plt.ylabel(y_label)
plt.show()

Adding errorbars to 3D plot in matplotlib

I can't find a way to draw errorbars in a 3D scatter plot in matplotlib.
Basically, for the following piece of code
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
X, Y, Z = axes3d.get_test_data(1)
ax.scatter(X, Y, zs = Z, zdir = 'z')
I am looking for something like
ax.errorbar(X,Y, zs = Z, dY, dX, zserr = dZ)
Is there a way to do this in mplot3d? If not, are there other libraries with this function?
There is clearly example on forum http://mple.m-artwork.eu/home/posts/simple3dplotwith3derrorbars
Here is the code but is not built-in functionality:
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d.axes3d as axes3d
fig = plt.figure(dpi=100)
ax = fig.add_subplot(111, projection='3d')
#data
fx = [0.673574075,0.727952994,0.6746285]
fy = [0.331657721,0.447817839,0.37733386]
fz = [18.13629648,8.620699842,9.807536512]
#error data
xerror = [0.041504064,0.02402152,0.059383144]
yerror = [0.015649804,0.12643117,0.068676131]
zerror = [3.677693713,1.345712547,0.724095592]
#plot points
ax.plot(fx, fy, fz, linestyle="None", marker="o")
#plot errorbars
for i in np.arange(0, len(fx)):
ax.plot([fx[i]+xerror[i], fx[i]-xerror[i]], [fy[i], fy[i]], [fz[i], fz[i]], marker="_")
ax.plot([fx[i], fx[i]], [fy[i]+yerror[i], fy[i]-yerror[i]], [fz[i], fz[i]], marker="_")
ax.plot([fx[i], fx[i]], [fy[i], fy[i]], [fz[i]+zerror[i], fz[i]-zerror[i]], marker="_")
#configure axes
ax.set_xlim3d(0.55, 0.8)
ax.set_ylim3d(0.2, 0.5)
ax.set_zlim3d(8, 19)
plt.show()
I ended up writing the method for matplotlib: official example for 3D errorbars:
import matplotlib.pyplot as plt
import numpy as np
ax = plt.figure().add_subplot(projection='3d')
# setting up a parametric curve
t = np.arange(0, 2*np.pi+.1, 0.01)
x, y, z = np.sin(t), np.cos(3*t), np.sin(5*t)
estep = 15
i = np.arange(t.size)
zuplims = (i % estep == 0) & (i // estep % 3 == 0)
zlolims = (i % estep == 0) & (i // estep % 3 == 2)
ax.errorbar(x, y, z, 0.2, zuplims=zuplims, zlolims=zlolims, errorevery=estep)
ax.set_xlabel("X label")
ax.set_ylabel("Y label")
ax.set_zlabel("Z label")
plt.show()

Categories

Resources