Python Matplotlib Multicursor: Show value under cursor in legend - python

I am using Multicursor to get a cursor on every graph.
I want to show the value of the datapoint, which is hit by the cursor, inside a legend during hovering over the graphs, like this
Actually I have thought that this is a standard feature of matplotlib respectively Multicursor, but it seems not. Did someone already something like this or do I have to implement it by my own.
I already found this post matplotlib multiple values under cursor, but this could be just the beginning for the implementation I want.

I have developed a solution.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import MultiCursor
from bisect import bisect_left
fig = plt.figure(figsize=(15, 8))
# create random graph with 60 datapoints, 0 till 59
x = list(range(0,60))
axes_list = []
def createRandomGraph(ax,x):
y = np.random.randint(low=0, high=15, size=60)
data.append(y)
ax.plot(x,y, marker='.')
def take_closest(myList, myNumber):
"""
Assumes myList is sorted. Returns closest value to myNumber.
If two numbers are equally close, return the smallest number.
"""
pos = bisect_left(myList, myNumber)
if pos == 0:
return myList[0]
if pos == len(myList):
return myList[-1]
before = myList[pos - 1]
after = myList[pos]
if after - myNumber < myNumber - before:
return after, pos
else:
return before, pos-1
def show_Legend(event):
#get mouse coordinates
mouseXdata = event.xdata
# the value of the closest data point to the current mouse position shall be shown
closestXValue, posClosestXvalue = take_closest(data[0], mouseXdata)
i = 1
for ax in axes_list:
datalegend = ax.text(1.05, 0.5, data[i][posClosestXvalue], fontsize=7,
verticalalignment='top', bbox=props, transform=ax.transAxes)
ax.draw_artist(datalegend)
# this remove is required because otherwise after a resizing of the window there is
# an artifact of the last label, which lies behind the new one
datalegend.remove()
i +=1
fig.canvas.update()
# store the x value of the graph in the first element of the list
data = [x]
# properties of the legend labels
props = dict(boxstyle='round', edgecolor='black', facecolor='wheat', alpha=1.5)
for i in range(5):
if(i>0):
# all plots share the same x axes, thus during zooming and panning
# we will see always the same x section of each graph
ax = plt.subplot(5, 1, i+1, sharex=ax)
else:
ax = plt.subplot(5, 1, i+1)
axes_list.append(ax)
createRandomGraph(ax,x)
multi = MultiCursor(fig.canvas, axes_list, color='r', lw=1)
# function show_Legend is called while hovering over the graphs
fig.canvas.mpl_connect('motion_notify_event', show_Legend)
plt.show()
The output looks like this
Maybe you like it and find it useful

Related

Making parts of a line graph a different colour depending on their y value in Matplotlib

I'm making a program which takes a random list of data and will plot it.
I want the colour of the graph to change if it goes above a certain value.
https://matplotlib.org/gallery/lines_bars_and_markers/multicolored_line.html
Matplotlib has an entry on doing just this but it seems to require using a function as input for the graph not using lists.
Does anyone know how to either convert this to work for lists or another way of doing so?
Here's my code so far (without my horrific failed attempts to colour code them)
from matplotlib import pyplot as plt
import random
import sys
import numpy as np
#setting the max and min values where I want the colour to change
A_min = 2
B_max = 28
#makes lists for later
A_min_lin = []
B_max_lin = []
#simulating a corruption of the data where it returns all zeros
sim_crpt = random.randint(0,10)
print(sim_crpt)
randomy = []
if sim_crpt == 0:
randomy = []
#making the empty lists for corrupted data
for i in range(0,20):
randomy.append(0)
print(randomy)
else:
#making a random set of values for the y axis
for i in range(0,20):
n = random.randint(0,30)
randomy.append(n)
print(randomy)
#making an x axis for time
time = t = np.arange(0, 20, 1)
#Making a list to plot a straight line showing where the maximum and minimum values
for i in range(0, len(time)):
A_min_lin.append(A_min)
B_max_lin.append(B_max)
#Testing to see if more than 5 y values are zero to return if it's corrupted
tracker = 0
for i in (randomy):
if i == 0:
tracker += 1
if tracker > 5:
sys.exit("Error, no data")
#ploting and showing the different graphs
plt.plot(time,randomy)
plt.plot(time,A_min_lin)
plt.plot(time,B_max_lin)
plt.legend(['Data', 'Minimum for linear', "Maximum for linear"])
plt.show
You can use np.interp to generate the fine-grain data to plot:
# fine grain time
new_time = np.linspace(time.min(), time.max(), 1000)
# interpolate the y values
new_randomy = np.interp(new_time, time, randomy)
# this is copied from the link with few modification
points = np.array([new_time, new_randomy]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
fig, axs = plt.subplots()
norm = plt.Normalize(new_randomy.min(), new_randomy.max())
lc = LineCollection(segments, cmap='viridis', norm=norm)
# Set the values used for colormapping
lc.set_array(new_randomy[1:])
lc.set_linewidth(2)
line = axs.add_collection(lc)
fig.colorbar(line, ax=axs)
# set the limits
axs.set_xlim(new_time.min(), new_time.max())
axs.set_ylim(new_randomy.min(), new_randomy.max())
plt.show()
Output:

Shifting the origin in ruptures.display

I am trying to perform change point detection using the ruptures package. When I use the ruptures.display for plotting, the x axis starts of with 0 as the start point.
And here is how the plot looks like:
However, I would like to start with an offset. Therefore I have tried to create a custom display function using the ruptures.display source code. But, I am not able to figure out how to shift the origin.
Below is the main code:
data = pd.read_csv("test_flooding.csv")
Start_time = pd.to_datetime('81028520.26',unit='s')
End_time = pd.to_datetime('81113495.41',unit='s')
#Format the 'Date' column
data['Time']=data['Time'].astype(str)
#Convert the Date column into a date object
data['Time']=pd.to_datetime(data['Time'],unit='s')
#Selecting a specific range
data=data[(data['Time']<=End_time)]
data=data[(Start_time <=data['Time'])]
data = data.loc[data['ID'] == "id1"]
#Convert the time series values to a numpy 1D array
points=np.array(data['Signal1_of_ID'])
#RUPTURES PACKAGE
#Changepoint detection with the Pelt search method
start_timestamp = int(time.mktime(Start_time.timetuple()))
model="rbf"
algo = rpt.Pelt(model=model).fit(points)
result = algo.predict(pen=10)
display(points,start_timestamp , result, figsize=(10, 6))
plt.title('Change Point Detection: Pelt Search Method')
plt.show()
And here is the custom display code:
from itertools import cycle
import matplotlib.pyplot as plt
import numpy as np
from ruptures.utils import pairwise
COLOR_CYCLE = ["#4286f4", "#f44174"]
def display(signal,Start_time, true_chg_pts, computed_chg_pts=None, **kwargs):
"""
Display a signal and the change points provided in alternating colors. If another set of change
point is provided, they are displayed with dashed vertical dashed lines.
Args:
signal (array): signal array, shape (n_samples,) or (n_samples, n_features).
true_chg_pts (list): list of change point indexes.
computed_chg_pts (list, optional): list of change point indexes.
Returns:
tuple: (figure, axarr) with a :class:`matplotlib.figure.Figure` object and an array of Axes objects.
"""
if signal.ndim == 1:
signal = signal.reshape(-1, 1)
n_samples, n_features = signal.shape
# let's set all options
figsize = (10, 2 * n_features) # figure size
alpha = 0.2 # transparency of the colored background
color = "k" # color of the lines indicating the computed_chg_pts
linewidth = 3 # linewidth of the lines indicating the computed_chg_pts
linestyle = "--" # linestyle of the lines indicating the computed_chg_pts
if "figsize" in kwargs:
figsize = kwargs["figsize"]
if "alpha" in kwargs:
alpha = kwargs["alpha"]
if "color" in kwargs:
color = kwargs["color"]
if "linewidth" in kwargs:
linewidth = kwargs["linewidth"]
if "linestyle" in kwargs:
linestyle = kwargs["linestyle"]
fig, axarr = plt.subplots(n_features, figsize=figsize, sharex=True)
if n_features == 1:
axarr = [axarr]
for axe, sig in zip(axarr, signal.T):
color_cycle = cycle(COLOR_CYCLE)
# plot s
axe.plot(range(Start_time,Start_time+n_samples), sig)
# color each (true) regime
bkps = [0] + sorted(true_chg_pts)
for (start, end), col in zip(pairwise(bkps), color_cycle):
axe.axvspan(max(0, start - 0.5),
end - 0.5,
facecolor=col, alpha=alpha)
# vertical lines to mark the computed_chg_pts
if computed_chg_pts is not None:
for bkp in computed_chg_pts:
if bkp != 0 and bkp < n_samples:
axe.axvline(x=bkp - 0.5,
color=color,
linewidth=linewidth,
linestyle=linestyle)
fig.tight_layout()
return fig, axarr
Here is how the image looks with my custom display trial, which still plots with the origin as 0:
Any help is highly appreciated.
It looks like your code might include zero, since you're prepending zero to the break points list:
axe.axvspan(max(0, start - 0.5),
end - 0.5,
facecolor=col, alpha=alpha)
should probably use your Start_time instead of zero:
axe.axvspan(max(Start_time, start - 0.5),
end - 0.5,
facecolor=col, alpha=alpha)

Updating items on a grid dynamically within a for loop (python)

I’m trying to update a plot dynamically within a for loop and I can’t get it to work. I wonder if anyone can help?
I get a bit confused between passing the figure vs axes and how to update. I’ve been trying to use
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(2)
but it’s not doing what I want it to.
I'm trying to:
1. add objects to a grid (setupGrid2)
2. at a timestep - move each object in random direction (makeMove2)
3. update the position of each object visually on the grid (updateGrid2)
My problem is with 3. I'd like to clear the previous step, so that just the new location for each object is displayed. The goal to show the objects dynamically moving around the grid.
I'd also like to work with the ax object created in setupGrid2, so that I can set the plot variables (title, legend etc.) in one place and update that chart.
Grateful for any help.
Sample code below (for running in jupyter notebook):
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
import pylab as pl
from IPython import display
def setupGrid2(norows,nocols,noobjects):
#each object needs current grid position (x and y coordinate)
objects = np.zeros(noobjects)
ObjectPos = np.zeros(shape=(noobjects,2))
#put objects randomly on grid
for i in range (noobjects):
ObjectPos[i][0] = np.random.uniform(0,norows)
ObjectPos[i][1] = np.random.uniform(0,nocols)
#plot objects on grid
fig = plt.figure(1,figsize=(15,5))
ax = fig.add_subplot(1,1,1)
x,y = zip(*ObjectPos)
ax.scatter(x, y,c="b", label='Initial positions')
ax.grid()
plt.show()
return ax,ObjectPos
def updateGrid2(ax,ObjPos):
x,y = zip(*ObjPos)
plt.scatter(x, y)
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(0.1)
#move object in a random direction
def makeMove2(object,xpos,ypos):
#gets a number: 1,2,3 or 4
direction = int(np.random.uniform(1,4))
if (direction == 1):
ypos = ypos+1
if (direction == 2):
ypos = ypos - 1
if (direction == 3):
xpos = xpos+1
if (direction == 4):
xpos = xpos-1
return xpos,ypos
def Simulation2(rows,cols,objects,steps):
ax,ObjPos = setupGrid2(rows,cols,objects)
for i in range(steps):
for j in range (objects):
xpos = ObjPos[j][0]
ypos = ObjPos[j][1]
newxpos,newypos = makeMove2(j,xpos,ypos)
ObjPos[j][0] = newxpos
ObjPos[j][1] = newypos
updateGrid2(ax,ObjPos)
Simulation2(20,20,2,20)
It seems you want to update the scatter, instead of producing a new scatter for each frame. That would be shown in this question. Of course you can still use display when running this in jupyter instead of the shown solutions with ion or FuncAnimation.
Leaving the code from the question mostly intact this might look as follows.
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
import pylab as pl
from IPython import display
def setupGrid2(norows,nocols,noobjects):
#each object needs current grid position (x and y coordinate)
objects = np.zeros(noobjects)
ObjectPos = np.zeros(shape=(noobjects,2))
#put objects randomly on grid
for i in range (noobjects):
ObjectPos[i,0] = np.random.uniform(0,norows)
ObjectPos[i,1] = np.random.uniform(0,nocols)
#plot objects on grid
fig = plt.figure(1,figsize=(15,5))
ax = fig.add_subplot(1,1,1)
ax.axis([0,nocols+1,0,norows+1])
x,y = zip(*ObjectPos)
scatter = ax.scatter(x, y,c="b", label='Initial positions')
ax.grid()
return ax,scatter,ObjectPos
def updateGrid2(ax,sc,ObjPos):
sc.set_offsets(ObjPos)
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(0.1)
#move object in a random direction
def makeMove2(object,xpos,ypos):
#gets a number: 1,2,3 or 4
direction = int(np.random.uniform(1,4))
if (direction == 1):
ypos = ypos+1
if (direction == 2):
ypos = ypos - 1
if (direction == 3):
xpos = xpos+1
if (direction == 4):
xpos = xpos-1
return xpos,ypos
def Simulation2(rows,cols,objects,steps):
ax,scatter,ObjPos = setupGrid2(rows,cols,objects)
for i in range(steps):
for j in range (objects):
xpos = ObjPos[j,0]
ypos = ObjPos[j,1]
newxpos,newypos = makeMove2(j,xpos,ypos)
ObjPos[j,0] = newxpos
ObjPos[j,1] = newypos
updateGrid2(ax,scatter,ObjPos)
Simulation2(20,20,3,20)

finding local maximum from fft of a signal

I'm trying to find a peak of an fft of a signal to be used for a further analysis of the signal. I'm using a SpanSelect of data and doing an fft, represented as a frequency spectrum. I really wanted to have the plot be interactive and the user click a point to be further analyzed, but I don't see a way to do that so would like a way to find local frequency peaks. The frequency spectrum may look like this:
So I would want a way to return the frequency that has a peak at 38 hz for example. Is there a way to do this?
use argrelextrema for finding local maxima:
import numpy as np
from scipy.signal import argrelextrema
from matplotlib.pyplot import *
np.random.seed()
x = np.random.random(50)
m = argrelextrema(x, np.greater) #array of indexes of the locals maxima
y = [x[m] for i in m]
plot(x)
plot(m, y, 'rs')
show()
You can do something like that using matplotlib widgets, for example check out the lasso method of selecting points.
You can then use the selected point in any form of analysis you need.
EDIT: Combined lasso and SpanSelect widget from matplotlib examples
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector, LassoSelector
from matplotlib.path import Path
import matplotlib.pyplot as plt
try:
raw_input
except NameError:
# Python 3
raw_input = input
class SelectFromCollection(object):
"""Select indices from a matplotlib collection using `LassoSelector`.
Selected indices are saved in the `ind` attribute. This tool highlights
selected points by fading them out (i.e., reducing their alpha values).
If your collection has alpha < 1, this tool will permanently alter them.
Note that this tool selects collection objects based on their *origins*
(i.e., `offsets`).
Parameters
----------
ax : :class:`~matplotlib.axes.Axes`
Axes to interact with.
collection : :class:`matplotlib.collections.Collection` subclass
Collection you want to select from.
alpha_other : 0 <= float <= 1
To highlight a selection, this tool sets all selected points to an
alpha value of 1 and non-selected points to `alpha_other`.
"""
def __init__(self, ax, collection, alpha_other=0.3):
self.canvas = ax.figure.canvas
self.collection = collection
self.alpha_other = alpha_other
self.xys = collection.get_offsets()
self.Npts = len(self.xys)
# Ensure that we have separate colors for each object
self.fc = collection.get_facecolors()
if len(self.fc) == 0:
raise ValueError('Collection must have a facecolor')
elif len(self.fc) == 1:
self.fc = np.tile(self.fc, self.Npts).reshape(self.Npts, -1)
self.lasso = LassoSelector(ax, onselect=self.onselect)
self.ind = []
def onselect(self, verts):
path = Path(verts)
self.ind = np.nonzero([path.contains_point(xy) for xy in self.xys])[0]
self.fc[:, -1] = self.alpha_other
self.fc[self.ind, -1] = 1
self.collection.set_facecolors(self.fc)
self.canvas.draw_idle()
def disconnect(self):
self.lasso.disconnect_events()
self.fc[:, -1] = 1
self.collection.set_facecolors(self.fc)
self.canvas.draw_idle()
def onselect(xmin, xmax):
indmin, indmax = np.searchsorted(x, (xmin, xmax))
indmax = min(len(x)-1, indmax)
thisx = x[indmin:indmax]
thisy = y[indmin:indmax]
line2.set_data(thisx, thisy)
ax2.set_xlim(thisx[0], thisx[-1])
ax2.set_ylim(thisy.min(), thisy.max())
fig.canvas.draw()
if __name__ == '__main__':
plt.ion()
fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(211, axisbg='#FFFFCC')
x = np.arange(0.0, 5.0, 0.01)
y = np.sin(2*np.pi*x) + 0.5*np.random.randn(len(x))
ax.plot(x, y, '-')
ax.set_ylim(-2,2)
ax.set_title('Press left mouse button and drag to test')
ax2 = fig.add_subplot(212, axisbg='#FFFFCC')
line2, = ax2.plot(x, y, '-')
pts = ax2.scatter(x, y)
# set useblit True on gtkagg for enhanced performance
span = SpanSelector(ax, onselect, 'horizontal', useblit=True,
rectprops=dict(alpha=0.5, facecolor='red') )
selector = SelectFromCollection(ax2, pts)
plt.draw()
raw_input('Press any key to accept selected points')
print("Selected points:")
print(selector.xys[selector.ind])
selector.disconnect()
# Block end of script so you can check that the lasso is disconnected.
raw_input('Press any key to quit')

Matplotlib/Pyplot: How to zoom subplots together AND x-scroll separately?

I previously asked the question "How to zoom subplots together?", and have been using the excellent answer since then.
I'm now plotting just two sets of time-series data, and I need to continue to zoom as above, but now I need to also pan one plot relative to the other (I'm doing eyeball correlation). The data comes from 2 independent instruments with different start times and different clock settings.
In use, I zoom using the 'Zoom to Rectangle' toolbar button, and I scroll using the "Pan/Zoom" button.
How may I best scroll one plot in X relative to the other? Ideally, I'd also like to capture and display the time difference. I do not need to scroll vertically in Y.
I suspect I may need to stop using the simple "sharex=" "sharey=" method, but am not certain how best to proceed.
Thanks, in advance, to the great StackOverflow community!
-BobC
I hacked the above solution until it did want I think I want.
# File: ScrollTest.py
# coding: ASCII
"""
Interatively zoom plots together, but permit them to scroll independently.
"""
from matplotlib import pyplot
import sys
def _get_limits( ax ):
""" Return X and Y limits for the passed axis as [[xlow,xhigh],[ylow,yhigh]]
"""
return [list(ax.get_xlim()), list(ax.get_ylim())]
def _set_limits( ax, lims ):
""" Set X and Y limits for the passed axis
"""
ax.set_xlim(*(lims[0]))
ax.set_ylim(*(lims[1]))
return
def pre_zoom( fig ):
""" Initialize history used by the re_zoom() event handler.
Call this after plots are configured and before pyplot.show().
"""
global oxy
oxy = [_get_limits(ax) for ax in fig.axes]
# :TODO: Intercept the toolbar Home, Back and Forward buttons.
return
def re_zoom(event):
""" Pyplot event handler to zoom all plots together, but permit them to
scroll independently. Created to support eyeball correlation.
Use with 'motion_notify_event' and 'button_release_event'.
"""
global oxy
for ax in event.canvas.figure.axes:
navmode = ax.get_navigate_mode()
if navmode is not None:
break
scrolling = (event.button == 1) and (navmode == "PAN")
if scrolling: # Update history (independent of event type)
oxy = [_get_limits(ax) for ax in event.canvas.figure.axes]
return
if event.name != 'button_release_event': # Nothing to do!
return
# We have a non-scroll 'button_release_event': Were we zooming?
zooming = (navmode == "ZOOM") or ((event.button == 3) and (navmode == "PAN"))
if not zooming: # Nothing to do!
oxy = [_get_limits(ax) for ax in event.canvas.figure.axes] # To be safe
return
# We were zooming, but did anything change? Check for zoom activity.
changed = None
zoom = [[0.0,0.0],[0.0,0.0]] # Zoom from each end of axis (2 values per axis)
for i, ax in enumerate(event.canvas.figure.axes): # Get the axes
# Find the plot that changed
nxy = _get_limits(ax)
if (oxy[i] != nxy): # This plot has changed
changed = i
# Calculate zoom factors
for j in [0,1]: # Iterate over x and y for each axis
# Indexing: nxy[x/y axis][lo/hi limit]
# oxy[plot #][x/y axis][lo/hi limit]
width = oxy[i][j][1] - oxy[i][j][0]
# Determine new axis scale factors in a way that correctly
# handles simultaneous zoom + scroll: Zoom from each end.
zoom[j] = [(nxy[j][0] - oxy[i][j][0]) / width, # lo-end zoom
(oxy[i][j][1] - nxy[j][1]) / width] # hi-end zoom
break # No need to look at other axes
if changed is not None:
for i, ax in enumerate(event.canvas.figure.axes): # change the scale
if i == changed:
continue
for j in [0,1]:
width = oxy[i][j][1] - oxy[i][j][0]
nxy[j] = [oxy[i][j][0] + (width*zoom[j][0]),
oxy[i][j][1] - (width*zoom[j][1])]
_set_limits(ax, nxy)
event.canvas.draw() # re-draw the canvas (if required)
pre_zoom(event.canvas.figure) # Update history
return
# End re_zoom()
def main(argv):
""" Test/demo code for re_zoom() event handler.
"""
import numpy
x = numpy.linspace(0,100,1000) # Create test data
y = numpy.sin(x)*(1+x)
fig = pyplot.figure() # Create plot
ax1 = pyplot.subplot(211)
ax1.plot(x,y)
ax2 = pyplot.subplot(212)
ax2.plot(x,y)
pre_zoom( fig ) # Prepare plot event handler
pyplot.connect('motion_notify_event', re_zoom) # for right-click pan/zoom
pyplot.connect('button_release_event',re_zoom) # for rectangle-select zoom
pyplot.show() # Show plot and interact with user
# End main()
if __name__ == "__main__":
# Script is being executed from the command line (not imported)
main(sys.argv)
# End of file ScrollTest.py
Ok, here's my stab at it. This works, but there might be a simpler approach. This solution uses some matplotlib event-handling to trigger a new set_xlim() every time it notices the mouse in motion. The trigger event 'motion_notify_event' could be eliminated if dynamic synchronous zooming isn't required.
Bonus: this works for any number of subplots.
from matplotlib import pyplot
import numpy
x = numpy.linspace(0,10,100)
y = numpy.sin(x)*(1+x)
fig = pyplot.figure()
ax1 = pyplot.subplot(121)
ax1.plot(x,y)
ax2 = pyplot.subplot(122)
ax2.plot(x,y)
ax1.old_xlim = ax1.get_xlim() # store old values so changes
ax2.old_xlim = ax2.get_xlim() # can be detected
def re_zoom(event):
zoom = 1.0
for ax in event.canvas.figure.axes: # get the change in scale
nx = ax.get_xlim()
ox = ax.old_xlim
if ox != nx: # of axes that have changed scale
zoom = (nx[1]-nx[0])/(ox[1]-ox[0])
for ax in event.canvas.figure.axes: # change the scale
nx = ax.get_xlim()
ox = ax.old_xlim
if ox == nx: # of axes that need an update
mid = (ox[0] + ox[1])/2.0
dif = zoom*(ox[1] - ox[0])/2.0
nx = (mid - dif, mid + dif)
ax.set_xlim(*nx)
ax.old_xlim = nx
if zoom != 1.0:
event.canvas.draw() # re-draw the canvas (if required)
pyplot.connect('motion_notify_event', re_zoom) # for right-click pan/zoom
pyplot.connect('button_release_event', re_zoom) # for rectangle-select zoom
pyplot.show()

Categories