How to update text in matplotlib with serial data from Arduino? - matplotlib

I am attempting to create an animated plot that updates in real time with the data from my serial port. Data is streamed in by an Arduino in an 8x8 array. The data are temperatures from an IR camera. I am able to create an instance of a figure but I cannot get the text to update with the serial stream data.
I tried to set 'plt.show(block=False)' so that the script would continue, but this makes the figure empty completely and scales it into a small window with a loading cursor that just continues to load.
I only want the text to update with the array data, as well as the colors from the new normalized data.
How can I get the text to update with the serial data in matplotlib?
Thanks!
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import serial
import time
tempdata = serial.Serial("COM3",9600)
tempdata.timeout = 100
strn = []
rows = ["A", "B", "C", "D",
"E", "F", "G","H"]
columns = ["1", "2", "3", "4",
"5", "6", "7","8"]
print("AMG8833 8x8 Infrared Camera")
time.sleep(0.75)
print("Connected to: " + tempdata.portstr)
time.sleep(0.75)
print("Initializing Camera...")
tempsArray = np.empty((8,8))
while True: #Makes a continuous loop to read values from Arduino
fig, ax = plt.subplots()
im = ax.imshow(tempsArray,cmap='plasma')
tempdata.flush()
strn = tempdata.read_until(']') #reads the value from the serial port as a string
tempsString = np.asarray(strn)
tempsFloat = np.fromstring(tempsString, dtype=float, sep= ', ')
# Axes ticks
ax.set_xticks(np.arange(len(columns)))
ax.set_yticks(np.arange(len(rows)))
# Axes labels
ax.set_xticklabels(columns)
ax.set_yticklabels(rows)
# Rotate the tick labels and set their alignment.
plt.setp(ax.get_xticklabels(), rotation=45, ha="right",
rotation_mode="anchor")
tempsArray.flat=tempsFloat
im.set_array(tempsArray)
ax.set_title("")
fig.tight_layout()
#Loop over data dimensions and create text annotations.
for i in range(len(rows)):
for j in range(len(columns)):
text = ax.text(j, i, tempsArray[i, j],
ha="center", va="center", color="w")
plt.show()
Heat Map

This dynamic updating can be achieved with matplotlib's interactive mode. The answer to your question is very similar to this one: basically you need to enable interactive mode with ion() and then update the plot without calling the show() (or correlated) function.
Also, the plot and subplots are to be created only once, before the input loop.
This is the modified example:
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import serial
import time
tempdata = serial.Serial("COM3",9600)
tempdata.timeout = 100
strn = []
rows = ["A", "B", "C", "D",
"E", "F", "G","H"]
columns = ["1", "2", "3", "4",
"5", "6", "7","8"]
print("AMG8833 8x8 Infrared Camera")
time.sleep(0.75)
print("Connected to: " + tempdata.portstr)
time.sleep(0.75)
print("Initializing Camera...")
tempsArray = np.empty((8,8))
plt.ion()
fig, ax = plt.subplots()
# The subplot colors do not change after the first time
# if initialized with an empty matrix
im = ax.imshow(np.random.rand(8,8),cmap='plasma')
# Axes ticks
ax.set_xticks(np.arange(len(columns)))
ax.set_yticks(np.arange(len(rows)))
# Axes labels
ax.set_xticklabels(columns)
ax.set_yticklabels(rows)
# Rotate the tick labels and set their alignment.
plt.setp(ax.get_xticklabels(), rotation=45, ha="right",
rotation_mode="anchor")
ax.set_title("")
fig.tight_layout()
text = []
while True: #Makes a continuous loop to read values from Arduino
tempdata.flush()
strn = tempdata.read_until(']') #reads the value from the serial port as a string
tempsString = np.asarray(strn)
tempsFloat = np.fromstring(tempsString, dtype=float, sep= ', ')
tempsArray.flat=tempsFloat
im.set_array(tempsArray)
#Delete previous annotations
for ann in text:
ann.remove()
text = []
#Loop over data dimensions and create text annotations.
for i in range(len(rows)):
for j in range(len(columns)):
text.append(ax.text(j, i, tempsArray[i, j],
ha="center", va="center", color="w"))
# allow some delay to render the image
plt.pause(0.1)
plt.ioff()
Note: this code worked for me, but since I don't have an Arduino right now I tested it with a randomly generated sequence of frames (np.random.rand(8,8,10)), so I might have overlooked some detail. Let me know how it works.

Related

xarray : how to stack several pcolormesh figures above a map?

For a ML project I'm currently on, I need to verify if the trained data are good or not.
Let's say that I'm "splitting" the sky into several altitude grids (let's take 3 values for the moment) and for a given region (let's say, Europe).
One grid could be a signal reception strength (RSSI), another one the signal quality (RSRQ)
Each cell of the grid is therefor a rectangle and it has a mean value of each measurement (i.e. RSSI or RSRQ) performed in that area.
I have hundreds of millions of data
In the code below, I know how to draw a coloured mesh with xarray for each altitude: I just use xr.plot.pcolormesh(lat,lon, the_data_set); that's fine
But this will only give me a "flat" figure like this:
RSSI value at 3 different altitudes
I need to draw all the pcolormesh() of a dataset for each altitude in such way that:
1: I can have the map at the bottom
2: Each pcolormesh() is stacked and "displayed" at its altitude
3: I need to add a 3d scatter plot for testing my trained data
4: Need to be interactive as I have to zoom in areas
For 2 and 3 above, I managed to do something using plt and cartopy :
enter image description here
But plt/cartopy combination is not as interactive as plotly.
But plotly doesn't have the pcolormesh functionality
And still ... I don't know in anycase, how to "stack" the pcolormesh results that I did get above.
I've been digging Internet for few days but I didn't find something that could satisfy all my criteria.
What I did to get my pcolormesh:
import numpy as np
import xarray as xr
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
class super_data():
def __init__(self, lon_bound,lat_bound,alt_bound,x_points,y_points,z_points):
self.lon_bound = lon_bound
self.lat_bound = lat_bound
self.alt_bound = alt_bound
self.x_points = x_points
self.y_points = y_points
self.z_points = z_points
self.lon, self.lat, self.alt = np.meshgrid(np.linspace(self.lon_bound[0], self.lon_bound[1], self.x_points),
np.linspace(self.lat_bound[0], self.lat_bound[1], self.y_points),
np.linspace(self.alt_bound[0], self.alt_bound[1], self.z_points))
self.this_xr = xr.Dataset(
coords={'lat': (('latitude', 'longitude','altitude'), self.lat),
'lon': (('latitude', 'longitude','altitude'), self.lon),
'alt': (('latitude', 'longitude','altitude'), self.alt)})
def add_data_array(self,ds_name,ds_min,ds_max):
def create_temp_data(ds_min,ds_max):
data = np.random.randint(ds_min,ds_max,size=self.y_points * self.x_points)
return data
temp_data = []
# Create "z_points" number of layers in the z axis
for i in range(self.z_points):
temp_data.append(create_temp_data(ds_min,ds_max))
data = np.concatenate(temp_data)
data = data.reshape(self.z_points,self.x_points, self.y_points)
self.this_xr[ds_name] = (("altitude","longitude","latitude"),data)
def plot(self,dataset, extent=None, plot_center=False):
# I want t
if np.sqrt(self.z_points) == np.floor(np.sqrt(self.z_points)):
side_size = int(np.sqrt(self.z_points))
else:
side_size = int(np.floor(np.sqrt(self.z_points) + 1))
fig = plt.figure()
i_ax=1
for i in range(side_size):
for j in range(side_size):
if i_ax < self.z_points+1:
this_dataset = self.this_xr[dataset].sel(altitude=i_ax-1)
# Initialize figure with subplots
ax = fig.add_subplot(side_size, side_size, i_ax, projection=ccrs.PlateCarree())
i_ax += 1
ax.coastlines()
this_dataset.plot.pcolormesh('lon', 'lat', ax=ax, infer_intervals=True, alpha=0.5)
else:
break
plt.tight_layout()
plt.show()
if __name__ == "__main__":
# Wanted coverage :
lons = [-15, 30]
lats = [35, 65]
alts = [1000, 5000]
xarr = super_data(lons,lats,alts,10,8,3)
# Add some fake data
xarr.add_data_array("RSSI",-120,-60)
xarr.add_data_array("pressure",700,1013)
xarr.plot("RSSI",0)
Thanks for you help

Utilise a slider to update the position of legend in Matplotlib

I am trying to make a slider that can adjust the x and y coordinates of the legend anchor, but this does not seem to be updating on the plot. I keep getting the message in console "No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument", each time the slider value is updated.
Here is the code, taken from this example in the matplotlib docs
from cProfile import label
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button
# The parametrized function to be plotted
def f(t, amplitude, frequency):
return amplitude * np.sin(2 * np.pi * frequency * t)
t = np.linspace(0, 1, 1000)
# Define initial parameters
init_amplitude = 5
init_frequency = 3
# Create the figure and the line that we will manipulate
fig, ax = plt.subplots()
line, = ax.plot(t, f(t, init_amplitude, init_frequency), lw=2, label = "wave")
ax.set_xlabel('Time [s]')
# adjust the main plot to make room for the sliders
fig.subplots_adjust(left=0.25, bottom=0.25)
initx = 0.4
inity = 0.2
def l(x,y):
return (x,y)
legend = fig.legend(title = 'title', prop={'size': 8}, bbox_to_anchor = l(initx,inity))
legend.remove( )
# Make a horizontal slider to control the frequency.
axfreq = fig.add_axes([0.25, 0.1, 0.3, 0.3])
freq_slider = Slider(
ax=axfreq,
label='Frequency [Hz]',
valmin=0.1,
valmax=30,
valinit=init_frequency,
)
# Make a vertically oriented slider to control the amplitude
axamp = fig.add_axes([0.1, 0.25, 0.0225, 0.63])
amp_slider = Slider(
ax=axamp,
label="Amplitude",
valmin=0,
valmax=10,
valinit=init_amplitude,
orientation="vertical"
)
# The function to be called anytime a slider's value changes
def update(val):
legend = plt.legend(title = '$J_{xx}$', prop={'size': 8}, bbox_to_anchor= l(amp_slider.val, freq_slider.val))
legend.remove()
#line.set_ydata(f(t, amp_slider.val, freq_slider.val))
fig.canvas.draw_idle()
# register the update function with each slider
freq_slider.on_changed(update)
amp_slider.on_changed(update)
# Create a `matplotlib.widgets.Button` to reset the sliders to initial values.
resetax = fig.add_axes([0.8, 0.025, 0.1, 0.04])
button = Button(resetax, 'Reset', hovercolor='0.975')
def reset(event):
freq_slider.reset()
amp_slider.reset()
button.on_clicked(reset)
plt.show()
Is it even possible to update other matplotlib plot parameters like xticks/yticks or xlim/ylim with a slider, rather than the actual plotted data? I am asking so that I can speed up the graphing process, as I tend to lose a lot of time just getting the right plot parameters whilst making plots presentable, and would like to automate this in some way.

Why is only one hatch used in the bar graph?

I have the following bar graph generated using pandas. My problem is all the bars have the same pattern. I have tried many approaches but could not manage to get around this issue.
Moreover, only one entry(for the last subplot) is shown in the legend.
The data used is
The code is :
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import ScalarFormatter
class ScalarFormatterForceFormat(ScalarFormatter):
def _set_format(self): # Override function that finds format to use.
self.format = "%1.1f" # Give format here
patterns = [ "\\" , "/" , "-","+" ,"x", "|", '.', "O" ]
yfmt = ScalarFormatterForceFormat()
yfmt.set_powerlimits((0, 0))
bar_gap=0.005
bar_width=0.01
bar_pos = [0 for i in range(5)]
bar_pos[0]=bar_gap
for i in range(1,5):
bar_pos[i]=bar_pos[i-1]+bar_gap+bar_width
colors = ['tab:blue', 'tab:green', 'tab:orange', 'tab:red','tab:olive']
patterns = [ "\\" , "/" , "+" , "-", ".", "*","x", "o", "O" ]
# file_locn = ''r'C:\Users\girum\Desktop\Throughput.csv'''
file_locn = ''r'my_file.csv'''
df = pd.read_csv(file_locn,index_col='Set')
df=df.T
fig, axes = plt.subplots(1,3,figsize=(8,5))#,sharey=True)
for i in range(3):
axes[i].yaxis.set_major_formatter(yfmt)
df.Type_A.plot(ax=axes[0],kind='bar',color=colors)
df.Type_B.plot(ax=axes[1],kind='bar',color=colors)
df.Type_C.plot(ax=axes[2],kind='bar',color=colors)
handles, labels = axes[0].get_legend_handles_labels()
for ax in fig.axes:
bars = ax.patches
hatches = ''.join(h*len(df) for h in patterns)
for bar, hatch in zip(bars, hatches):
bar.set_hatch(2*hatch)
plt.xticks(rotation=360)
axes[0].set_ylabel('Speed')
for i in range(len(df)):
axes[i].set_xlabel('')#Why is this line not working
axes[i].tick_params(axis='x', rotation=360)
plt.legend(loc='center right', bbox_to_anchor=(.2,1.08), ncol=1)
plt.show()
The code below has the following changes:
added some dummy test data to enable stand-alone test code
removed some unused variables
used the unaltered ScalarFormatter
only one loop through the axes and avoiding the plt interface
using ax.containers[0] to catch the bar container (ax.patches is a list of the rectangles, without the surrounding container)
change the label of the bar container to _no_legend, so it doesn't appear in the legend
used the patterns directly instead of concatenating them
removed h*len(df); note that multiplying a string such as '/' by e.g. 4, repeats the string (to '////'); repeated patterns are used in matplotlib to make the base pattern denser
used tick_params(axis='x', labelbottom=False, length=0) to remove the tick labels
added labels to the individual bars so they appear into the legend
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import ScalarFormatter
yfmt = ScalarFormatter()
yfmt.set_powerlimits((-9, 9))
colors = ['tab:blue', 'tab:green', 'tab:orange', 'tab:red', 'tab:olive']
patterns = ["\\", "/", "+", "-", ".", "*", "x", "o", "O"]
df = pd.DataFrame(np.random.randint(100000, 500000, (3, 3)),
columns=['A', 'B', 'C'],
index=['Type_A', 'Type_B', 'Type_C'])
df = df.T
fig, axes = plt.subplots(1, 3, figsize=(8, 5))
df.Type_A.plot(ax=axes[0], kind='bar', color=colors)
df.Type_B.plot(ax=axes[1], kind='bar', color=colors)
df.Type_C.plot(ax=axes[2], kind='bar', color=colors)
for ax in axes:
bars = ax.containers[0]
bars.set_label('_no_legend')
hatches = [h * 2 for h in patterns]
for bar, hatch, label in zip(bars, hatches, df.index):
bar.set_hatch(2 * hatch)
bar.set_label(label)
ax.yaxis.set_major_formatter(yfmt)
ax.tick_params(axis='x', labelbottom=False, length=0)
axes[0].set_ylabel('Speed')
axes[2].legend(loc='lower right', bbox_to_anchor=(1, 1.01), ncol=3)
plt.tight_layout()
plt.show()
The lines where you are joining the patterns generates a result, which you don't want.
patterns = [ "\\" , "/" , "+" , "-", ".", "*","x", "o", "O" ]
hatches = ''.join(h*3 for h in patterns)
>>> '\\\\\\///+++---...***xxxoooOOO'
# if you have the bars, this is the output
for bar, hatch in zip([0,1,3], hatches):
print(2*hatch)
>>>
\\
\\
\\
Try to simplify this section using the patterns in your loop directly:
for bar, hatch in zip([0,1,3], patterns):
print(2*hatch)`
>>>
\\
//
++
Output
I used your given code and data to create this output.

Matplotlib: Multiple plots with same layout (no automatic layout)

I am trying to make several pie charts that I can then transition between in a presentation. For this, it would be very useful for the automatic layouting to... get out of the way. The problem is that whenever I change a label, the whole plot moves around on the canvas so that it fits perfectly. I'd like the plot to stay centered, so it occupies the same area every time. I have tried adding center=(0,0) to ax.pie(), but to no avail.
Two examples:
Image smaller, left
Image larger, right
Instead of that effect, I'd like the pie chart to be in the middle of the canvas and have the same size in both cases (and I'd then manually make sure that the labels are on canvas by setting large margins).
The code I use to generate these two images is:
import matplotlib.pyplot as plt
import numpy as np
# Draw labels, from
# https://matplotlib.org/3.2.2/gallery/pie_and_polar_charts/pie_and_donut_labels.html#sphx-glr-gallery-pie-and-polar-charts-pie-and-donut-labels-py
def make_labels(ax, wedges, labs):
bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
kw = dict(arrowprops=dict(arrowstyle="-"),
bbox=bbox_props,
zorder=0, va="center")
for i, p in enumerate(wedges):
if p.theta2-p.theta1 < 5:
continue
ang = (p.theta2 - p.theta1) / 2. + p.theta1
y = np.sin(np.deg2rad(ang))
x = np.cos(np.deg2rad(ang))
horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
connectionstyle = "angle,angleA=0,angleB={}".format(ang)
kw["arrowprops"].update({"connectionstyle": connectionstyle})
ax.annotate(labs[i], xy=(x, y),
xytext=(1.1*x,1.1*y),
horizontalalignment=horizontalalignment, **kw)
kw=dict(autoscale_on=False, in_layout=False, xmargin=1, ymargin=1)
fig, ax = plt.subplots(figsize=(3, 3), dpi=100, subplot_kw=kw)
wedges, texts = ax.pie(x=[1,2,3], radius=1,
wedgeprops=dict(width=1),
pctdistance=0.7,
startangle=90,
textprops=dict(fontsize=8),
center=(0, 0))
make_labels(ax, wedges, ["long text", "b", "c"])
#make_labels(ax, wedges, ["a", "b", "long text"])
plt.show()
Thanks a lot in advance!
How are you saving your figures? It looks like you may be using savefig(..., bbox_inches='tight') which automatically resized the figure to include all the artists.
If I run your code with fig.savefig(..., bbox_inches=None), I get the following output

Python keeps overwriting hist on previous plot but doesn't save it with the desired plot

I am saving two separate figures, that each should contain 2 plots together.
The problem is that the first figure is ok, but the second one, does not gets overwritten on the new plot but on the previous one, but in the saved figure, I only find one of the plots :
This is the first figure , and I get the first figure correctly :
import scipy.stats as s
import numpy as np
import os
import pandas as pd
import openpyxl as pyx
import matplotlib
matplotlib.rcParams["backend"] = "TkAgg"
#matplotlib.rcParams['backend'] = "Qt4Agg"
#matplotlib.rcParams['backend'] = "nbAgg"
import matplotlib.pyplot as plt
import math
data = [336256, 620316, 958846, 1007830, 1080401]
pdf = array([ 0.00449982, 0.0045293 , 0.00455894, 0.02397463,
0.02395788, 0.02394114])
fig, ax = plt.subplots();
fig = plt.figure(figsize=(40,30))
x = np.linspace(np.min(data), np.max(data), 100);
plt.plot(x, s.exponweib.pdf(x, *s.exponweib.fit(data, 1, 1, loc=0, scale=2)))
plt.hist(data, bins = np.linspace(data[0], data[-1], 100), normed=True, alpha= 1)
text1= ' Weibull'
plt.savefig(text1+ '.png' )
datar =np.asarray(data)
mu, sigma = datar.mean() , datar.std() # mean and standard deviation
normal_std = np.sqrt(np.log(1 + (sigma/mu)**2))
normal_mean = np.log(mu) - normal_std**2 / 2
hs = np.random.lognormal(normal_mean, normal_std, 1000)
print(hs.max()) # some finite number
print(hs.mean()) # about 136519
print(hs.std()) # about 50405
count, bins, ignored = plt.hist(hs, 100, normed=True)
x = np.linspace(min(bins), max(bins), 10000)
pdfT = [];
for el in range (len(x)):
pdfTmp = (math.exp(-(np.log(x[el]) - normal_mean)**2 / (2 * normal_std**2)))
pdfT += [pdfTmp]
pdf = np.asarray(pdfT)
This is the second set :
fig, ax = plt.subplots();
fig = plt.figure(figsize=(40,40))
plt.plot(x, pdf, linewidth=2, color='r')
plt.hist(data, bins = np.linspace(data[0], data[-1], 100), normed=True, alpha= 1)
text= ' Lognormal '
plt.savefig(text+ '.png' )
The first plot saves the histogram together with curve. instead the second one only saves the curve
update 1 : looking at This Question , I found out that clearing the plot history will help the figures don't mixed up , but still my second set of plots, I mean the lognormal do not save together, I only get the curve and not the histogram.
This is happening, because you have set normed = True, which means that area under the histogram is normalized to 1. And since your bins are very wide, this means that the actual height of the histogram bars are very small (in this case so small that they are not visible)
If you use
n, bins, _ = plt.hist(data, bins = np.linspace(data[0], data[-1], 100), normed=True, alpha= 1)
n will contain the y-value of your bins and you can confirm this yourself.
Also have a look at the documentation for plt.hist.
So if you set normed to False, the histogram will be visible.
Edit: number of bins
import numpy as np
import matplotlib.pyplot as plt
rand_data = np.random.uniform(0, 1.0, 100)
fig = plt.figure()
ax_1 = fig.add_subplot(211)
ax_1.hist(rand_data, bins=10)
ax_2 = fig.add_subplot(212)
ax_2.hist(rand_data, bins=100)
plt.show()
will give you two plots similar (since its random) to:
which shows how the number of bins changes the histogram.
A histogram visualises the distribution of your data along one dimension, so not sure what you mean by number of inputs and bins.