Live Plotting Best Practices?

I have a project where I’m receiving time domain data in packets, that I’d like to plot live. After looking at some options, I decided to go with plain GR.jl. My simulation of MVP code is at the end of my post.

To run the code, include that file, and then run the function

gr_live_window()

You can type q and <ENTER> at the terminal to stop the live plotting.

I like GR because I want other people to run this code and GR seems like one of the easiest plotting libraries to install. There are some purposeful comments in there if others want to experiment with this code. Also, I’m making a distinction between an “animated plot” and a “live plot”. I need a “live plot”, and the ability to save that “live plot” as a movie or animation is a nice-to-have.

What I’m really wondering is if there’s a way to do this more elegantly or efficiently with GR. Some issues are

  • I put an ‘fps_update’ option in there and the effect is really neat, but the performance gets very sluggish about 20-30 seconds in to the plotting.
  • The numbers on the y-axis can get weird since those numbers are automatically chosen based on the data range

Thanks in advance!

MVP link gist: https://gist.github.com/standarddeviant/a1be022413563275b5cac84461ce93a7
MVP code:

# gr_live_window.jl
using GR

function keyboard_channel()
    # allow 'q' and <ENTER> sequentially pressed on the keyboard to break the loop
    count = 0
    kb2loop = Channel(10)
    @async while true
        kb_input = readline(STDIN)
        # maybe we hit an error or something got messed up... 
        # just break and relinquish STDIN
        if (count+=1) >= 5
            break
        end

        if contains(lowercase(kb_input), "q")
            put!(kb2loop, "quit")
            break
        end
    end
    return kb2loop
end

function gr_custom_plot(x, y; xylims=nothing)
    GR.setviewport(0.03, 0.97, 0.05, 0.95)
    # GR.setlinecolorind(218)
    # GR.setfillintstyle(1)
    # GR.setfillcolorind(208)
    # GR.updatews()

    if xylims==nothing
        xylims = (minimum(x), maximum(x), minimum(y), maximum(y))
    end

    GR.clearws()
    GR.setwindow(xylims...)
    GR.fillrect(xylims...)
    GR.grid(
        (xylims[2]-xylims[1])/10, 
        (xylims[4]-xylims[3])/10,
        xylims[1], 0, 5, 5
    )
    GR.axes( 
        1, #(xylims[2]-xylims[1])/30, 
        (xylims[4]-xylims[3])/30,
        xylims[1], xylims[3], 
        5, 5, 0.01
    )
    GR.polyline(x, y)
    # GR.polymarker(x, y)
    GR.updatews()
end

 

function gr_live_window(; timeout=3600, fps_update=false)
    # convenient constants
    fs = 10
    winsec = 30
    hopsec = 1
    nwin = round(Integer, winsec*fs)
    nhop = round(Integer, hopsec*fs)

    # let user interrupt the live plot
    kb2loop = keyboard_channel()

    # do the loop
    frame_start = -winsec
    frame_time = collect( (0:(nwin-1)) * (1/fs) )

    print("I: Warming up the plotting engine... "); t1=time()
    gr_custom_plot(frame_start + frame_time, zeros(Float32, nwin))
    println("FINISHED [$(round(time()-t1,3)) s]")

    fix = 0 # frame index
    # f0 = 0.5 # Hz
    fps_slide = 30
    tslide = 1/fps_slide
    start_time = time()
    aframe = zeros(Float32, nwin)
    while time() - start_time < timeout
        if isready(kb2loop) && "quit"==take!(kb2loop)
            break
        end

        # aframe = sin.(2*pi*f0.*(frame_start + frame_time))
        append!(aframe, randn(nhop)); deleteat!(aframe, 1:nhop)
        println("fix = $(fix+=1) @ $(time() - start_time)")
        gr_custom_plot(frame_start + frame_time, aframe)
        
        # this attempts to smoothly update at some fps or frames-per-second
        if fps_update
            xylims = [frame_start, frame_start+frame_time[end], minimum(aframe), maximum(aframe)]
            for ix=1:fps_slide
                # 0.9 is just a guess of (1-x) where x is the ratio
                #     (time of gr_custom_plot())  / (1/fps)
                sleep(tslide)
                xylims[1:2] += (tslide)
                gr_custom_plot(frame_start + frame_time, aframe; xylims=xylims)
            end
        else
            sleep(hopsec)
        end
        frame_start += hopsec
    end # while loop
    println("Finished in $(time() - start_time) seconds.")
end

I switched to using PyPlot.jl because PyPot.jl ended up being a lot simpler to use, and seems to be plenty fast for my purposes. I’m happy to post a an example if anyone’s interested.

Yes, please, post it.

Okay, here’s a simple version that just updates once per second:

# pyplot_live.jl
using PyPlot

function keyboard_channel()
    # allow 'q' and <ENTER> sequentially pressed on the keyboard to break the loop
    count = 0
    kb2loop = Channel(10)
    @async while true
        kb_input = readline(STDIN)
        # maybe we hit an error or something got messed up... 
        # just break and relinquish STDIN
        if (count+=1) >= 5
            break
        end

        if contains(lowercase(kb_input), "q")
            put!(kb2loop, "quit")
            break
        end
    end
    return kb2loop
end

function py_custom_plot(x::Vector{<:Real}, y::Vector{<:Real})
    (xLO, xHI) = (minimum(x), maximum(x))
    fsize = 12
    lwidth = 3
    PyPlot.clf()
    PyPlot.xlim(xLO, xHI)
    PyPlot.plot(x, y, lw=lwidth)
    PyPlot.grid()
    PyPlot.tick_params(axis="both", which="major", labelsize=fsize)
    PyPlot.xlabel("seconds", fontsize=fsize)
    PyPlot.ylabel("amplitude", fontsize=fsize)
end

 

function pyplot_live(; timeout=100)
    # convenient constants
    f0 = 1/10
    fs = 10
    winsec = 30
    hopsec = 1
    nwin = round(Integer, winsec*fs)
    nhop = round(Integer, hopsec*fs)

    f=figure() # 
    # let user interrupt the live plot
    kb2loop = keyboard_channel()

    # do the loop
    frame_start = -winsec
    frame_time = collect( (0:(nwin-1)) * (1/fs) )

    print("I: Warming up the plotting engine... "); t1=time()
    py_custom_plot(frame_start + frame_time, zeros(Float32, nwin))
    println("FINISHED [$(round(time()-t1,3)) s]")

    start_time = time()
    aframe = zeros(Float32, nwin)
    fix = 0
    while time() - start_time < timeout
        if isready(kb2loop) && "quit"==take!(kb2loop)
            break
        end

        aframe = sin.(2*pi*f0.*(frame_start + frame_time))
        # append!(aframe, randn(nhop)); deleteat!(aframe, 1:nhop)
        println("fix = $(fix+=1) @ $(time() - start_time)")
        py_custom_plot(frame_start + frame_time, aframe)
        
        # # this attempts to smoothly update at some fps or frames-per-second
        # if fps_update
        #     xylims = [frame_start, frame_start+frame_time[end], minimum(aframe), maximum(aframe)]
        #     for ix=1:fps_slide
        #         # 0.9 is just a guess of (1-x) where x is the ratio
        #         #     (time of gr_custom_plot())  / (1/fps)
        #         sleep(tslide)
        #         xylims[1:2] += (tslide)
        #         gr_custom_plot(frame_start + frame_time, aframe; xylims=xylims)
        #     end
        # else
        sleep(hopsec)
        #end
        frame_start += hopsec
    end # while loop
    println("Finished in $(time() - start_time) seconds.")
end

This is how it would look with makie:

function makie_live(;     
                f0 = 1/2, fs = 100,
                winsec = 4, hopsec = 1/60
            )
            nwin = round(Integer, winsec*fs)
            nhop = round(Integer, hopsec*fs)
            # do the loop
            frame_start = -winsec
            frame_time = collect((0:(nwin-1)) * (1/fs))
            aframe = sin.(2*pi*f0.*(frame_start .+ frame_time))
            scene = lines(frame_start .+ frame_time, aframe)
            display(scene)
            lineplot = scene[end]
            fix = 0
            i = 0
            while isopen(scene) && i < 50 # dont loop longer than 50x for testing
                aframe .= sin.(2*pi*f0.*(frame_start .+ frame_time))
                # append!(aframe, randn(nhop)); deleteat!(aframe, 1:nhop)
                lineplot[1] = frame_start .+ frame_time
                lineplot[2] = aframe
                AbstractPlotting.update_limits!(scene)
                AbstractPlotting.update!(scene)
                sleep(hopsec)
                frame_start += hopsec
            end # while loop
            scene
        end
        makie_live()


currently only works on Makie & AbstractPlotting master… Will hopefully tag next week!

8 Likes

You can also check for keyboard buttons like this:

ispressed(scene, Keyboard.q)::Bool
# or register a callback

on(events(scene).keyboardbuttons) do button
    @show ispressed(button, Keyboard.f5)
end
4 Likes