0

I followed this Add labels to real-time signal plotting with Vispy thread and tried using the example from https://vispy.org/gallery/scene/line_update.html#sphx-glr-gallery-scene-line-update-py . How can I hold the plotted information, I should be able to look at the past data as well by panning the view. Should I store the plotted data else where and retrieve that to look at the old data? I am trying to hold all information as new data gets added to the view. Any suggestions on how to do this will be really helpful.

Thanks.

Update 1:

'''

import sys
import numpy as np
from vispy import app, scene

# vertex positions of data to draw
N = 100
pos = np.zeros((N, 2), dtype=np.float32)
x_lim = [0, 10.]
y_lim = [-2., 2.]
pos[:, 0] = np.linspace(x_lim[0], x_lim[1], N)
pos[:, 1] = np.random.normal(size=N)

# color array
color = np.ones((N, 4), dtype=np.float32)
color[:, 0] = np.linspace(0, 1, N)
color[:, 1] = color[::-1, 0]

canvas = scene.SceneCanvas(keys='interactive', show=True)
grid = canvas.central_widget.add_grid(spacing=0)

viewbox = grid.add_view(row=0, col=1, camera='panzoom')

# add some axes
x_axis = scene.AxisWidget(orientation='bottom')
x_axis.stretch = (1, 0.1)
grid.add_widget(x_axis, row=1, col=1)
x_axis.link_view(viewbox)
y_axis = scene.AxisWidget(orientation='left')
y_axis.stretch = (0.1, 1)
grid.add_widget(y_axis, row=0, col=0)
y_axis.link_view(viewbox)

# add a line plot inside the viewbox
line = scene.Line(pos, color, parent=viewbox.scene)

# auto-scale to see the whole line.
viewbox.camera.set_range()
import time
t0 = time.time()
x_val = []
y_val= []



def update(ev):
    '''
     New x and y coordinates are generated and assigned to `pos`. 
     `datastore` variable holds all the data (past and current 
     generated data). Only `pos` data is used to update the plot in
     real time. 
    '''

    global pos, color, line, x_val, y_val,datastore
    
    
    pos[:, 0] = np.linspace(x_lim[0], x_lim[1], N)+time.time()-t0
    pos[:, 1] = np.sin(pos[:, 0])+0.2*np.random.normal(size=(N))
    
    
    x_val= np.append(x_val,pos[:,0])
    y_val= np.append(y_val,pos[:,1])
    datastore=np.stack((x_val, y_val),axis=1)
#     print(pos.size)
    
    color = np.roll(color, 1, axis=0)
    line.set_data(pos=pos)
    
    viewbox.camera.set_range(y=(pos[:, 1].max(), pos[:, 1].min()), x=(pos[:, 0].max(), pos[:, 0].min()))
    
    
def mouseMove(ev):
   '''
     `datastore` consists of all data. Based on the camera view, 
      data is extracted from `datastore`. this extracted data is 
      used to update the plot.
    '''
    
    
    print(viewbox.camera.get_state()['rect'])
    currentBounds= viewbox.camera.get_state()['rect']
    currentMin_x = currentBounds.right
    currentMax_x = currentBounds.left

    
    if currentMin_x < datastore[:,0].min():
        currentMin_x = datastore[:,0].min()
    if currentMax_x > datastore[:,0].max():
        currentMax_x = datastore[:,0].max()

    getData = datastore[(datastore[:,0]> currentMin_x) & (datastore[:,0]< currentMax_x)][:100] #Should be equal to N, for code to work
    
    print(getData.size)
    line.set_data(pos=getData, color=color) #Updates plot with the extracted data
    viewbox.camera.set_range(y=(getData[:, 1].max(), getData[:, 1].min()), x=(getData[:, 0].max(), getData[:, 0].min()))
    
    
print(viewbox.camera.get_state())
  
t = app.Timer()
t.connect(update)
t.start(iterations=60*5)
viewbox.events.mouse_release.connect(mouseMove)
t.events.stop.connect(lambda x: app.quit())

if __name__ == '__main__' and sys.flags.interactive == 0:
    app.run()

'''

Update 2

import sys
import numpy as np
from vispy import app, scene

# vertex positions of data to draw
N = 100
pos = np.zeros((N, 2), dtype=np.float32)
x_lim = [0, 10.]
y_lim = [-2., 2.]
pos[:, 0] = np.linspace(x_lim[0], x_lim[1], N)
pos[:, 1] = np.random.normal(size=N)



canvas = scene.SceneCanvas(keys='interactive', show=True)
grid = canvas.central_widget.add_grid(spacing=0)

viewbox = grid.add_view(row=0, col=1, camera='panzoom')

# add some axes
x_axis = scene.AxisWidget(orientation='bottom')
x_axis.stretch = (1, 0.1)
grid.add_widget(x_axis, row=1, col=1)
x_axis.link_view(viewbox)
y_axis = scene.AxisWidget(orientation='left')
y_axis.stretch = (0.1, 1)
grid.add_widget(y_axis, row=0, col=0)
y_axis.link_view(viewbox)

# add a line plot inside the viewbox
line = scene.Line(pos,  parent=viewbox.scene)

# auto-scale to see the whole line.
viewbox.camera.set_range()
import time
t0 = time.time()
x_val = []
y_val= []



def update(ev):
    global pos, line, x_val, y_val,datastore
    
    
    pos[:, 0] = np.linspace(x_lim[0], x_lim[1], N)+time.time()-t0
    pos[:, 1] = np.sin(pos[:, 0])+0.2*np.random.normal(size=(N))
    
    
    x_val= np.append(x_val,pos[:,0])
    y_val= np.append(y_val,pos[:,1])
    datastore=np.stack((x_val, y_val),axis=1)
#     print(pos.size)
    
    line.set_data(pos=pos)
    
    viewbox.camera.set_range(x=(pos[:, 0].min(),pos[:, 0].max()),y=(pos[:, 1].min(),pos[:, 1].max()))
    
    
def mouseMove(ev):
    
    
    print(viewbox.camera.get_state()['rect'])
    currentBounds= viewbox.camera.get_state()['rect']
    currentMin_x = currentBounds.left
    currentMax_x = currentBounds.right


    print(currentMin_x, currentMax_x)
    getData = datastore[(datastore[:,0]>currentMin_x) & (datastore[:,0]< currentMax_x)] #Should be equal to N, for code to work
    print(getData)
    print(getData.size)
    line.set_data(pos=getData)
    viewbox.camera.set_range( x=( getData[:, 0].min(),getData[:, 0].max()),y=(getData[:, 1].min(),getData[:, 1].max()))
    
    
print(viewbox.camera.get_state())
  
t = app.Timer()
t.connect(update)
t.start(iterations=60*15)
viewbox.events.mouse_release.connect(mouseMove)
t.events.stop.connect(lambda x: app.quit())

if __name__ == '__main__' and sys.flags.interactive == 0:
    app.run()
aven
  • 11
  • 2

1 Answers1

0

Overall I think this depends how you want your application to function as far as user experience and what kind of toll you want to have on the user's system. You have a couple choices:

  1. Hold on to all data in the GPU. Due to the way OpenGL and VisPy work this means you'd also have to have all that data on the CPU so you can update the overall array when new data comes in and then upload the whole array to the GPU. There are ways to only upload a portion of the data you want to send, but I can't remember if they work out of the box for the LineVisual used here or for the use case you have in mind (new data taking the place of old data).
  2. Hold on to all the data in the CPU and only a subset in the GPU, automatically updating the GPU data as the user interacts. This would require hooking pieces together so you know when the camera panned around and updating the data on the GPU. Not terrible, but a little complicated.
  3. Same as 2 but force the user to manually update data ranges with other UI elements (ex. Qt dropdown menus, buttons, etc). This way you don't have to hook things up for interactivity, you just update the data that you need.

This also leads me to ask about one concern I always have when people want applications like yours: do you need to show all the data? Would showing the last N records (even if only by default) be good enough? This makes it much easier on you, the application developer, at the cost of flexibility for the user that they probably won't even notice in most cases.

I should point out in the example you linked to, if it wasn't clear already, these are the lines where the data array are being updated with new data and then .set_data is called to upload the data to the GPU on the next draw:

    pos[:, 1] = np.random.normal(size=N)
    color = np.roll(color, 1, axis=0)
    line.set_data(pos=pos, color=color)

The color is just a fake modification of the array to change the color. The random data is just generating fake data and only updating the Y coordinates. You would obviously have real data that you'd need to update, but if you only show the last N records then updating only the Y positions in this matter would be an easy and performant way to do it. There are plenty of tricks that can be done with numpy arrays to reduce the amount of copies of the data you need to make and get the best performance. Let me know if you have any questions.

djhoese
  • 3,567
  • 1
  • 27
  • 45
  • Thanks @djhoese for the elaborate answer, I am thinking of storing the stream data to .csv and retrieve it when the user pans the view to look at the past data. How can I store the past data in the GPU? Is there any placeholder to hold the data ( like we use set_data to send the data to GPU). In my case, I can just hold and display the last N records and load the old records when the user wants. Along with the Y coordinates, I need to update the X coordinates as well, with the current timestamp. I will look at the API to see if there is any way to update X and Y coordinates. Thank you so much. – aven Aug 17 '21 at 01:22
  • For storing past data in the GPU, there are fancier ways, but for sticking with what is available in the LineVisual-style interface it could be just as easy as storing more points and changing the view of your Canvas to only show what you want. As for your storage on disk, that seems reasonable but I would suggest not using .csv and instead use a binary format. Numpy allows you to write binary data to disk and read it as a "memmap" (or mmap) which would be much faster and require less storage on disk. – djhoese Aug 17 '21 at 13:26
  • I tried storing the data in a variable for now. I extracted the past data using the range bounds that I got by panning the view. I have few issues here, the plot with past data looks like a bunch of random lines, and I had to set the length of retrieved data to the number of vertex points (N) that I used to plot in the beginning, it is throwing an error when I tried to render all the data within the panned view. Here are the images of plot [before]: https://ibb.co/ng8Fk3Y , [after]: https://ibb.co/3y1W3Z4. Please find the code under `Update 1`. – aven Aug 18 '21 at 01:27
  • @aven Just to clarify, you are only trying to plot one line at this point, right? I notice that you use `pos` in the `update` method but also update `datastore`. You only pass `pos` to the line and then in your mouse handler you use `datastore`. I don't think `pos` and `datastore` have the same data in them. Please correct me if I'm misunderstanding something. – djhoese Aug 18 '21 at 14:19
  • the idea is to plot/render the past data when I pan the camera. `datastore` is like a file which consists of all the current and past data, each time I generate data and assign it to `pos`, I also append it to the `datastore`. So, when I move the camera, the extracted data should be rendered and fill the view. `getData` consists of extracted data and the line is updated with the `getData`. I hope this clarifies to some extent, I have also added comments to the code. Thanks. – aven Aug 18 '21 at 16:56
  • A couple things: You mentioned an error when trying to render all points? This is likely because you are not updating the `color` to match the size of the data points. Second, is there a reason your x and y limits in `set_range` are backwards? I would triple check your `datastore` after it is updated and make sure that `x` is continuously increasing. – djhoese Aug 18 '21 at 19:21
  • Thanks for pointing out the errors @djhoese, I have removed the color component and made some edits to the code. Now the plot is rendered when I pan the view, but there are a lot of connecting lines, I think the plot is rendered from the last point in the stream of data. I checked the `datastore`, x is increasing continuously. Please find the code under `update 2` . Here is the plot after panning the camera. [rendered plot]: https://ibb.co/VVYC7FP, Thanks. – aven Aug 19 '21 at 00:42
  • I ran your code and added `print("getData diff: ", (np.diff(getData[:, 0] > 0)).all())` to check whether the difference between all X coordinates is increasing. The result is `False`. Same for `datastore`. The main issue is this line in your code `pos[:, 0] = np.linspace(x_lim[0], x_lim[1], N)+time.time()-t0`. `time.time()` is in floating point seconds and you are calling this line multiple times a second. Your X's are overlapping. If I print the first and last X for the first two iterations I get `[0.2720039, 10.272004]` and `[0.32849503, 10.328495]`. – djhoese Aug 19 '21 at 12:07