3

I have a 2 min data set sampled at 10 Hz that I want to plot like an ECG in R: I want to create a plot with a 30 s window, plot the first 30 s from left to right in real time, then after 30 s, the data are plotted from the left to right again, progressively replacing the previous curve. With like a 1s interval between the old curve and the new one. And export as mp4. For now, I manage to have an animated plot in R, but not something like an ECG.

A reproducible data set:

dt <- 0.1                     
freq   <- 12 / 60             
start <- as.POSIXct("2025-11-19 13:00:00", tz = "UTC") + 0.001
t_sec <- seq(from = 0, by = dt, length.out = n_rows)
times <- start + t_sec
signal <- sin(2 * pi * freq * t_sec)
rep_dataset <- data.frame(Time = times,Individual = signal)
head(rep_dataset, 5)
           time Individual
 1 13:00:00.000  0.0000000
 2 13:00:00.100  0.1253331
 3 13:00:00.200  0.2486899
 4 13:00:00.300  0.3681245
 5 13:00:00.401  0.4817538

My current code:

library("dplyr")
library("lubridate")
library("ggplot2")
library("gganimate")
library("pracma")
library("tuneR")
library("seewave")

segment <- rep_dataset

# Normalize time relative to start
segment$Time0 <- as.numeric(segment$Time - segment$Time[1])
peak_data <- findpeaks(segment$Individual, minpeakheight = mean(segment$Individual) + sd(segment$Individual))
peak_idx <- peak_data[,2]
segment$peak <- FALSE
segment$peak[peak_idx] <- TRUE

#ECG-type line
offsets <- c(-3, -1.5, 0, 1.5, 3)

ECG <- ggplot(segment, aes(x = Time0, y = Individual))

for(off in offsets){
  ECG <- ECG + geom_line(
    aes(y = Individual + off),
    color = rgb(0.2, 0.4, 1, 0.15),   
    linewidth = 1.5
  )
}

ECG <- ECG + geom_line(
  aes(y = Individual),
  color = rgb(0.2, 0.4, 1, 1),
  linewidth = 1.5
)


# Add labels and animate
ECG <- ECG +
  theme_minimal(base_size = 15) +
  theme(
    panel.grid.minor = element_blank(),
    panel.grid.major = element_line(color = "gray25", linewidth = 0.2),
    plot.background = element_rect(fill = "black", color = NA),
    panel.background = element_rect(fill = "black", color = NA),
    text = element_text(color = "white"),
    axis.text = element_text(color = "white")
  ) +
  labs(subtitle = "{sprintf('%.2f', frame_along)}",
       x = "",
       y = "Electrical current"
  ) +
  transition_reveal(Time0)

#render a mp4 file
fps <- 10
nframes <- nrow(segment) 

animate(
  ECG,
  nframes = nframes,
  fps = fps,
  renderer = av_renderer("my_video.mp4"),
  width = 1000,
  height = 480)
1
  • I don't have a definitive answer, but I think the easiest way would be to create a variable that identifies each pulse of 30s, then use gganimate inside a loop to produce frames looping through the segments of 30s to essentially animate one plot for each "pulse", and in the end create the animation using all the images generated. Commented Nov 19 at 19:06

1 Answer 1

7

Here's a sort of brute force approach using dplyr/tidyr to make a custom data set for each frame. For small and medium sized data, this gives you near-complete flexibility to show whichever subsets of data you want at each step, but make take some effort to translate this into code.

I'd change two of your early lines first:

freq   <- 12.5 / 60   # so successive cycles have distinct values

# Time (subsequently used for calcs) should be numeric (datetime)
rep_dataset <- data.frame(Time = times, Individual = signal)  

Then we can pre-calc what to show each frame and animate it:

segment |>
  # copy of shifted data for each individual
  tidyr::expand_grid(offsets) |> 

  # copy of data for each timestamp, limited Time to the prior 29 seconds
  tidyr::expand_grid(timestamp = segment$Time) |>
  filter(Time <= timestamp, Time > timestamp - dseconds(29)) |>

  # plotted_time is the displayed x value, era tracks which cycle of data
  mutate(plotted_time = min(Time) + (Time - floor_date(Time, "30 seconds")),
         era = floor((Time - min(Time)) / dseconds(30))) |> 

  filter(timestamp <= min(timestamp) + dseconds(60)) |> # just demo first 2 cycles

ggplot(aes(plotted_time, Individual + offsets, 
           group = interaction(offsets, era))) + 
  geom_point(size = 0.3) +
  geom_line() +
  transition_manual(timestamp)

enter image description here


EDIT - It sounds like I misinterpreted the intention of the original code, and OP is looking for a type of fading code that highlights the most recent data being plotted. Here's one of many approaches for that. Here, I include points for the most recent 2 seconds of data, where size fades quadratically:

segment |>
  tidyr::expand_grid(timestamp = segment$Time) |>
  filter(timestamp >= Time,
         timestamp - dseconds(29) < Time) |>
  mutate(plotted_time = min(Time) + (Time - floor_date(Time, "30 seconds")),
         era = floor((Time - min(Time)) / dseconds(30))) |> 
  
  # only need to demonstrate with first two cycles
  filter(timestamp <= min(timestamp) + dseconds(60)) |>

ggplot(aes(plotted_time, Individual, group = era)) + 
  geom_point(aes(size = I(fade)),
             data = ~mutate(., age = (timestamp - Time) / dseconds(1),
                              fade = pmax(0, 2 - age)^2) |>
               filter(age <= 2)) +
  geom_line() +
  transition_manual(timestamp)

enter image description here

Or another variation:

...
  geom_line(aes(alpha = I(fade)),
             data = ~mutate(., age = (timestamp - Time) / dseconds(1),
                              fade = pmax(0, 10 - age)/10) |>
               filter(age <= 10)) +
  geom_line(alpha = 0.3) +
  transition_manual(timestamp)

enter image description here

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks @JonSpring, that is an interesting start. I would just need one line/curve though. Or one set of frames, then replaced by the next one, then the next one
It sounds like I misunderstood the intentions behind your for(off in offsets) loop -- it looked like you were trying to add a separate vertical series for each of five offset values. Is offsets just intended to add spikiness to the sin values?
Offset is intended qt introducing q gloying effect to the curve, to simulate an ECG. It's not the most critical at this point, I could also go without to just get the curve moving. Actually I think like you edited, but without the noise.
What's a "q gloying effect"? Do you mean "a glowing effect" -- like with ggfx.data-imaginist.com/reference/with_outer_glow.html? Please be as specific as you can be, as I don't understand yet.
Yes, sorry, my pc switched between langages, I did not notice. I mean a glowing effect indeed, like the part/dot being plotted is more brilliant. Like a real ECG monitor. This is what I was looking for. But that is also ok if we skip it. The most important part is the curve replacing itself, which it looks like you almost have

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.