Search code examples
chartsbrowserchart.js

ChartJS realtime waveform getting mixed up when chrome tab swtiched back and forth


Am having a simple application that constantly gets waveform data and displayed in my browser (chrome), chart.js mode is set to realtime. It works well when the chrome browser tab stays on, even when some other app's pop up happens on top of the browser tab, but when i minimize the browser and then restore or switch to other tabs and back, the waveform gets messed up (gaps or overwrites). Any recommendation how to prevent these?

Tried to see if any events can be handled when browser switches tabs, but its not straightforward handling the waveform continuously smooth..

example: Waveform demo

const ctx = document.getElementById('waveformChart').getContext('2d');
const POINTS_PER_SECOND = 100; // Number of points to generate per second
const WINDOW_SIZE = 10; // Window size in seconds
let startTime = Date.now();

const chart = new Chart(ctx, {
   type: 'line',
   data: {
      datasets: [{
         label: 'Waveform',
         data: [],
         borderColor: 'rgb(75, 192, 192)',
         borderWidth: 2,
         tension: 0.3,
         pointRadius: 0
      }]
   },
   options: {
      responsive: true,
      animation: false,
      interaction: {
         intersect: false
      },
      plugins: {
         legend: {
            display: false
         }
      },
      scales: {
         x: {
            type: 'linear',
            display: true,
            title: {
               display: true,
               text: 'Time (seconds)'
            },
            ticks: {
               maxRotation: 0,
               callback: function(value){
                  return Math.round(value);
               }
            },
            grid: {
               display: true
            }
         },
         y: {
            title: {
               display: true,
               text: 'Amplitude'
            },
            min: -1.5,
            max: 1.5
         }
      }
   }
});

function generateWaveformData(time){
   const frequency = 1; // 1 Hz
   const amplitude = 1.0;
   return amplitude * Math.sin(2 * Math.PI * frequency * time);
}

function updateChart(){
   const currentTime = (Date.now() - startTime) / 1000; // Convert to seconds
   
   // Generate new data point
   const newPoint = {
      x: currentTime,
      y: generateWaveformData(currentTime)
   };
   
   // Add new data point
   chart.data.datasets[0].data.push(newPoint);
   
   // Remove old data points (keep only last WINDOW_SIZE seconds)
   chart.data.datasets[0].data = chart.data.datasets[0].data.filter(point =>
      point.x > currentTime - WINDOW_SIZE
   );
   
   // Update x-axis limits to maintain fixed window
   chart.options.scales.x.min = currentTime - WINDOW_SIZE;
   chart.options.scales.x.max = currentTime;
   
   chart.update('quiet');
}

// Update at 60fps for smooth animation
function animate(){
   updateChart();
   requestAnimationFrame(animate);
}

// Start animation
animate();
.container {
   width: 90%;
   margin: 20px auto;
}

canvas {
   background-color: #f5f5f5;
   border-radius: 5px;
   padding: 10px;
}
<div class="container">
   <canvas id="waveformChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

When the tab is switched and back, you can see dents in the waveform.. same case with min/restore of browser


Solution

  • Your code has already implemented most of the features required to make it resilient to hidden-tab throttling. The most important thing that is already there, is the use of real time with the animation loop, and not some fictitious time based on animation frames count.

    One simple feature that can make the animation look OK after it was interrupted by the tab being hidden is to simply interrupt the curve, when the interval between two consecutive animation frames is larger than a preset interval - I used 0.1 or 1 / (frequency * 10) - regardless of the reason that happens. If there are less than 10 points per period, the curve won't look OK, so we better split it into a before and after that interval.

    Interrupting the curve can be achieved by inserting a null-valued point:

    let previousTime = null;
    function updateChart(){
       const currentTime = (Date.now() - startTime) / 1000; // Convert to seconds
    
       // Generate new data point
       const newPoint = {
          x: currentTime,
          y: generateWaveformData(currentTime)
       };
    
       const minDT = 0.1; // 1/(10*frequency)
       if(previousTime && currentTime - previousTime > minDT){
          // interrupt the line
          chart.data.datasets[0].data.push({x: currentTime - minDT, y: null});
       }
       previousTime = currentTime;
    
       // Add new data point
       chart.data.datasets[0].data.push(newPoint);
    
       // ........... the rest of the update as in the original post
    }
    

    The whole code as a stack snippet (I also added some minor changes to the x axis settings to make the animation smoother):

    const ctx = document.getElementById('waveformChart').getContext('2d');
    const POINTS_PER_SECOND = 100; // Number of points to generate per second
    const WINDOW_SIZE = 10; // Window size in seconds
    let startTime = Date.now();
    
    const chart = new Chart(ctx, {
       type: 'line',
       data: {
          datasets: [{
             label: 'Waveform',
             data: [],
             borderColor: 'rgb(75, 192, 192)',
             borderWidth: 2,
             tension: 0.3,
             pointRadius: 0
          }]
       },
       options: {
          responsive: true,
          animation: false,
          interaction: {
             intersect: false
          },
          plugins: {
             legend: {
                display: false
             }
          },
          scales: {
             x: {
                type: 'linear',
                display: true,
                title: {
                   display: true,
                   text: 'Time (seconds)'
                },
                ticks: {
                   maxRotation: 0,
                   includeBounds: false, // don't show non-integer bounds
                   callback: function(value, index, ticks){
                      if(index === ticks.length - 1 && this.max - value < 0.5){
                         // delay displaying the last tick to avoid stuttering
                         // produced by the label extending beyond the axis length
                         return null
                      }
                      return value;
                   }
                },
                grid: {
                   display: true
                }
             },
             y: {
                title: {
                   display: true,
                   text: 'Amplitude'
                },
                min: -1.5,
                max: 1.5
             }
          }
       }
    });
    
    function generateWaveformData(time){
       const frequency = 1; // 1 Hz
       const amplitude = 1.0;
       return amplitude * Math.sin(2 * Math.PI * frequency * time);
    }
    
    let previousTime = null;
    function updateChart(){
       const currentTime = (Date.now() - startTime) / 1000; // Convert to seconds
    
       // Generate new data point
       const newPoint = {
          x: currentTime,
          y: generateWaveformData(currentTime)
       };
    
       const minDT = 0.1; // 1/(10*frequency)
       if(previousTime && currentTime - previousTime > minDT){
          // interrupt the line
          chart.data.datasets[0].data.push({x: currentTime - minDT, y: null});
       }
       previousTime = currentTime;
    
       // Add new data point
       chart.data.datasets[0].data.push(newPoint);
    
       // Remove old data points (keep only last WINDOW_SIZE seconds)
       chart.data.datasets[0].data = chart.data.datasets[0].data.filter(point =>
          point.x > currentTime - WINDOW_SIZE
       );
    
       // Update x-axis limits to maintain fixed window
       chart.options.scales.x.min = currentTime - WINDOW_SIZE;
       chart.options.scales.x.max = currentTime;
    
       chart.update('quiet');
    }
    
    // Update at 60fps for smooth animation
    function animate(){
       updateChart();
       requestAnimationFrame(animate);
    }
    
    // Start animation
    animate();
    .container {
       width: 90%;
       margin: 20px auto;
    }
    
    canvas {
       background-color: #f5f5f5;
       border-radius: 5px;
       padding: 10px;
    }
    <div class="container">
       <canvas id="waveformChart"></canvas>
    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>


    Update.

    As suggested by @Sathish Kumar, an alternative solution is to generate all the missing points resulted from such an interruption.

    In fact, simply using a fixed number of points per second will simply take care of that (simplified piece of code, missing initialization, the full logic in the stack snippet below):

       // ...........................
       let newPoints = [];
    
       const nPoints = Math.floor((currentTime - lastPointTime) * POINTS_PER_SECOND);
       if(nPoints > 0){
          newPoints = Array.from({length: nPoints}, (_, i) => {
             const t = lastPointTime + (i + 1)/POINTS_PER_SECOND;
             return {
                x: t,
                y: generateWaveformData(t)
             }
          });
          lastPointTime = newPoints[nPoints - 1].x;
       }
       chart.data.datasets[0].data.push(...newPoints);
       // ...........................
    

    This will work fine with interruptions, but in the case an interruption is very long, it might create an unnecessarily large array of points, so an optimization might be prepended:

       // ...........................
       let newPoints = [];
       
       if(lastPointTime < currentTime - WINDOW_SIZE){
          // to avoid creating very large arrays after an interruption much longer than WINDOW_SIZE
          lastPointTime = lastPointTime + Math.floor(currentTime - WINDOW_SIZE - lastPointTime);
       }
       const nPoints = Math.floor((currentTime - lastPointTime) * POINTS_PER_SECOND);
       // ...........................
    

    Stack snippet:

    const ctx = document.getElementById('waveformChart').getContext('2d');
    const POINTS_PER_SECOND = 100; // Number of points to generate per second
    const WINDOW_SIZE = 10; // Window size in seconds
    let startTime = Date.now();
    
    const chart = new Chart(ctx, {
       type: 'line',
       data: {
          datasets: [{
             label: 'Waveform',
             data: [],
             borderColor: 'rgb(75, 192, 192)',
             borderWidth: 2,
             tension: 0.3,
             pointRadius: 0
          }]
       },
       options: {
          responsive: true,
          animation: false,
          interaction: {
             intersect: false
          },
          plugins: {
             legend: {
                display: false
             }
          },
          scales: {
             x: {
                type: 'linear',
                display: true,
                title: {
                   display: true,
                   text: 'Time (seconds)'
                },
                ticks: {
                   maxRotation: 0,
                   includeBounds: false, // don't show non-integer bounds
                   callback: function(value, index, ticks){
                      if(index === ticks.length - 1 && this.max - value < 0.5){
                         // delay displaying the last tick to avoid stuttering
                         // produced by the label extending beyond the axis length
                         return null
                      }
                      return value;
                   }
                },
                grid: {
                   display: true
                }
             },
             y: {
                title: {
                   display: true,
                   text: 'Amplitude'
                },
                min: -1.5,
                max: 1.5
             }
          }
       }
    });
    
    function generateWaveformData(time){
       const frequency = 1; // 1 Hz
       const amplitude = 1.0;
       return amplitude * Math.sin(2 * Math.PI * frequency * time);
    }
    
    let lastPointTime = null;
    function updateChart(){
       const currentTime = (Date.now() - startTime) / 1000; // Convert to seconds
    
       let newPoints = [];
       if(lastPointTime === null){
          newPoints = [{
             x: currentTime,
             y: generateWaveformData(currentTime)
          }];
          lastPointTime = currentTime;
       }
       else{
          if(lastPointTime < currentTime - WINDOW_SIZE){
             // to avoid creating very large arrays after an interruption much longer than WINDOW_SIZE
             lastPointTime = lastPointTime + Math.floor(currentTime - WINDOW_SIZE - lastPointTime);
          }
          const nPoints = Math.floor((currentTime - lastPointTime) * POINTS_PER_SECOND);
          if(nPoints > 0){
             newPoints = Array.from({length: nPoints}, (_, i) => {
                const t = lastPointTime + (i + 1)/POINTS_PER_SECOND;
                return {
                   x: t,
                   y: generateWaveformData(t)
                }
             });
             lastPointTime = newPoints[nPoints - 1].x;
          }
       }
    
       // Add new data point
       chart.data.datasets[0].data.push(...newPoints);
    
       // Remove old data points (keep only last WINDOW_SIZE seconds)
       chart.data.datasets[0].data = chart.data.datasets[0].data.filter(point =>
          point.x > currentTime - WINDOW_SIZE
       );
    
       // Update x-axis limits to maintain fixed window
       chart.options.scales.x.min = currentTime - WINDOW_SIZE;
       chart.options.scales.x.max = currentTime;
    
       chart.update('quiet');
    }
    
    // Update at 60fps for smooth animation
    function animate(){
       updateChart();
       requestAnimationFrame(animate);
    }
    
    // Start animation
    animate();
    .container {
       width: 90%;
       margin: 20px auto;
    }
    
    canvas {
       background-color: #f5f5f5;
       border-radius: 5px;
       padding: 10px;
    }
    <div class="container">
       <canvas id="waveformChart"></canvas>
    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>