Sunday, February 15, 2015

HTML5: High Performance Real Time Graphing using Canvas and requestAnimationFrame

Working with my WebView Android apps using ready-made javascript graphing libraries is frustrating. None of them really can handle real-time drawing because they redraw the entire frame every frame regardless of whether the frame remains the same or not. The demo oximeter app that I am working with suffered low frame rates at fastest refresh rate of 4 frames a second. The actual PPG data was coming in at least 50 Hz. Other pulse oximeters from M--- or N--- clocks in even higher at 62.5 Hz. Refreshing frames at 4 Hz? Yikes. And the fact is that these graphs should be simple and plain. Doctors use them to determine the quality of the oxygen saturation measurements. There's no need for axis labels or fancy shadings. As a consequence I had to redo the graphing and do so from scratch. I looked up tutorials for using HTML5 canvas and the requestAnimationFrame call to create a fast and efficient real time graphing solution. Only spent half a Sunday on it, it's that easy.

Here is an example using a random number generator (with hysteresis).

HTML5 Canvas not supported

Look at the smoothness on this baby.

I created this library script calling it PlethGraph.js. I named it pleth because I need it for PPG, but really you can use it for other real time graphing such as the first image of electrocardiographs (ECG) or others like electroencephalographs (EEG), or even seismography.

From what I gathered, the main concept with smooth animations in HTML is the use of requestAnimationFrame. Previously in my WebView I was doing that old-school setInterval method with javascript. And then I would set it at a fixed rate so that it would draw at those intervals. The problem with this is that the browser is a very a busy guy. He's got a lot of other things to process as well. If you tell him to get things done based on your time when he's got a lot on his plate, he's not going to get it finished. And when he can't get either done, things start lagging behind. So, you tell him to get it done when he's most efficient. That way, the browser can effectively do all the rendering as fast as possible.

And the use of this requestAnimationFrame passes an argument of the function it will call to recursively draw the next frame. In my code that I've attached at the very bottom you can see the object's start function which puts the graphing into animation loop. The first bit is just for browser compatibility.

    g.start = function() {
      reqAnimFrame =   window.requestAnimationFrame       ||
                       window.mozRequestAnimationFrame    ||
                       window.webkitRequestAnimationFrame ||
                       window.msRequestAnimationFrame     ||
                       window.oRequestAnimationFrame;
      
      // Recursive call to do animation frames
      reqAnimFrame(g.start);
      
      // We need to fill in data into the buffer so we know what to draw
      g.fillData();
      
      // Draw the frame (with the supplied data buffer)
      g.draw();
    };
    

Then you have the call to do the next draw which is, I guess, a non-blocking call. After that, since this is a graphing module, I need an input of data so it calls the fillData to grab the data from the user, and lastly it draws the frame ending this function. However, it really doesn't end because the recursive call reqAnimFrame(g.start) was already called so the next frame is either getting drawn or waiting to be drawn.

Just for fun, and because the lay person won't recognize PPG, I decided to draw up an ECG monitor simulator where a predefined ECG pulse is drawn with a random noise imposed.

HTML5 Canvas not supported

You can almost hear the beeping sounds.. Anyway, here's the complete code. Feel free to rip this and hack the heck out of it. It's still just a really simple library for myself to use so could be full of bugs. When you call the constructor, you pass two parameters. The first parameter is a the canvas ID that you want to graph on (so you have to create one yourself). The second parameter is the function that will return an array of data points. Even if you only have one point to draw in this frame, you still need to put it in an array of length 1.

/*
 * 
 * Photoplethysmograph (Real Time PPG Grapher)
 * 
 *    by: Tso (Peter) Chen
 * 
 * 
 * 
 * 0.1 - first version
 * 
 * 
 * Absolutely free to use, copy, edit, share, etc.
 *--------------------------------------------------*/
  
  /*
   * Helper function to convert a number to the graph coordinate
   * ----------------------------------------------------------- */
  function convertToGraphCoord(g, num){
    return (g.height / 2) * -(num * g.scaleFactor) + g.height / 2;
  }

  /*
   * Constructor for the PlethGraph object
   * ----------------------------------------------------------- */
  function PlethGraph(cid, datacb){
    
    var g             =   this;
    g.canvas_id       =   cid;
    g.canvas          =   $("#" + cid);
    g.context         =   g.canvas[0].getContext("2d");
    g.width           =   $("#" + cid).width();
    g.height          =   $("#" + cid).height();
    g.white_out       =   g.width * 0.01;
    g.fade_out        =   g.width * 0.15;
    g.fade_opacity    =   0.2;
    g.current_x       =   0;
    g.current_y       =   0;
    g.erase_x         =   null;
    g.speed           =   2;
    g.linewidth       =   1;
    g.scaleFactor     =   1;
    g.stop_graph      =   false;
    
    g.plethStarted    =   false;
    g.plethBuffer     =   new Array();
    
    /*
     * The call to fill the data buffer using
     * the data callback
     * ---------------------------------------- */
    g.fillData = function() {
      g.plethBuffer = datacb();
      };
      

    /*
     * The call to start the ging
     * ---------------------------------------- */
    g.start = function() {
      reqAnimFrame =   window.requestAnimationFrame       ||
                       window.mozRequestAnimationFrame    ||
                       window.webkitRequestAnimationFrame ||
                       window.msRequestAnimationFrame     ||
                       window.oRequestAnimationFrame;
      
      // Recursive call to do animation frames
      if (!g.stop_graph) reqAnimFrame(g.start);
      
      // We need to fill in data into the buffer so we know what to draw
      g.fillData();
      
      // Draw the frame (with the supplied data buffer)
      g.draw();
    };
    
    
    g.draw = function() {
      // Circle back the draw point back to zero when needed (ring drawing)
      g.current_x = (g.current_x > g.width) ? 0 : g.current_x;
      
      // "White out" a region before the draw point
      for( i = 0; i < g.white_out ; i++){
        g.erase_x = (g.current_x + i) % g.width;
        g.context.clearRect(g.erase_x, 0, 1, g.height);
      }
      
      // "Fade out" a region before the white out region
      for( i = g.white_out ; i < g.fade_out ; i++ ){
        g.erase_x = (g.current_x + i) % g.width;
        g.context.fillStyle="rgba(255, 255, 255, " + g.fade_opacity.toString() + ")";
        g.context.fillRect(g.erase_x, 0, 1, g.height);
      }
  
      // If this is first time, draw the first y point depending on the buffer
      if (!g.started) {
        g.current_y = convertToGraphCoord(g, g.plethBuffer[0]);
        g.started = true;
      }
      
      // Start the drawing
      g.context.beginPath();

      // We first move to the current x and y position (last point)
      g.context.moveTo(g.current_x, g.current_y);

      for (i = 0; i < g.plethBuffer.length; i++) {
        // Put the new y point in from the buffer
        g.current_y = convertToGraphCoord(g, g.plethBuffer[i]);
        
        // Draw the line to the new x and y point
        g.context.lineTo(g.current_x += g.speed, g.current_y);
        
        // Set the 
        g.context.lineWidth   = g.linewidth;
        g.context.lineJoin    = "round";
        
        // Create stroke
        g.context.stroke();
      }
      
      // Stop the drawing
      g.context.closePath();
    };
  }

9 comments :

  1. Hello,

    thanks for your tutorial.
    Is it possible to give me a executable example of your ECG monitor simulator as jsfiddle or for repl.it ? I later want to simulate an ECG with different situations (high/ low frequenz).

    Many thanks in advance!

    Sebastian

    ReplyDelete
  2. Hello sir,
    where we gonna add the dynamic data from Ajax/Json?

    ReplyDelete
  3. Hello,

    Thank you so much for this I have been learning several charting libraries over the last month none of which enabled me to achieve my desired results(a live streaming ECG graph). You code worked like a charm! I just have question. How would i make it so the heart rate can be assigned by the user. I understand how to make the interface for the user i just dont understand how what parameter is used to change the rate of the ecg graph. I tried changing the value of g.speed but this seemed to have no affect on my graph. If i want to increase/decrease the heart rate how can I achieve this?

    I posted my question to stack overflow:
    https://stackoverflow.com/questions/54635143/how-to-change-the-rate-of-my-graph-data-stream

    ReplyDelete
  4. awesome, will use it for an EMR prototype!

    ReplyDelete
  5. hi, how to pass the data to constructor .. please let me know and also is this only one class file ??

    ReplyDelete
  6. hlw please help how to use it and how can i put data from datase

    ReplyDelete
    Replies
    1. Hey I was having trouble with getting this working myself, but I found this that did the trick for me with some small modifications: http://jsfiddle.net/Shp4X/

      These are the changes I made to the JS code. You can change the canvas width and height in the html. Hope it helps :)


      var canvas = document.getElementById("canvas");
      var ctx = canvas.getContext("2d");
      ctx.fillStyle = "#dbbd7a";
      ctx.fill();

      var fps = 75;
      var n = 1;
      var data_count = 1;


      var data = [-99, -91, -80, -70, -60, -58, -57, -55, -55, -57, -61, -62, -56, -52, -54, -60, -65, -70, -67, -60, -51, -45, -37, -26, -22, -31, -42, -42, -35, -33, -41, -56, -59, -54, -48, -50, -51, -54, -50, -46, -45, -47, -53, -55, -49, -42, -40, -47, -50, -46, -42, -36, -42, -47, -57, -61, -67, -70, -65, -58, -61, -68, -61, -49, -50, -87, -118, -72, 66, 196, 188, 31, -147, -228, -205, -154, -118, -104, -90, -80, -74, -69, -65, -64, -65, -67, -65, -64, -66, -71, -72, -73, -76, -71, -64, -59, -60, -60, -56, -47, -43, -43, -44, -45, -44, -45, -54, -61, -64, -63, -60, -62, -65, -67, -67, -64, -61, -59, -57, -59, -61, -61, -61, -59, -55, -56, -59, -58, -56, -56, -55, -61, -68, -75, -78, -74, -68, -66, -64, -66, -65, -56, -49, -72, -116, -103, 22, 179, 213, 76, -121, -225, -217, -165, -127, -107, -95, -89, -78, -67, -65, -63, -64, -64, -65, -66, -64, -62, -66, -70, -70, -66, -63, -59, -58, -65, -57, -42, -32, -32, -35, -40, -41,];


      drawWave();

      function drawWave() {
      setTimeout(function() {
      requestAnimationFrame(drawWave);
      ctx.lineWidth = "2";
      ctx.strokeStyle = 'white';

      // Drawing code goes here
      n += 1;
      data_count += 1;
      if (n >= 1200) {
      n = 1;
      } //Change 1200 to the width you want the trace to stop and begin again.
      ctx.beginPath();
      ctx.moveTo(n - 1, data[data_count - 1]+400); // The +400 brings the height of the canvas down so that it is in the center of the screen.
      ctx.lineTo(n, data[data_count]+400);
      ctx.stroke();

      ctx.clearRect(n+1, 0, 5, canvas.height);

      }, 1000 / fps);
      }

      Delete
  7. Well Written Beautiful Post!
    here is my post related to html canvas.
    you may like.
    Convert HTML to Canvas

    ReplyDelete