Easier to read charts for Health App

Posted on
  • @Gordon
    I have been experimenting with a bar chart format that is a bit more readable for the Health App.
    I'm finding the existing charts too small to read.

    In the Day 9 example 1 - you can see the chart loads and highlights the previous day in the centre of the screen. This is the last day in the history.

    In the Day 7 example, I have swiped right twice to scroll back one day at a time and see the exact step count day by day. Of course you can swipe left again and reverse. In this way you could review a whole month. The day under view is always displayed in the centre of the screen as blue. The data is displayed below the chart. I actually nicked the idea from an android app that works this way. I also like the fact that the charts have attractive colors. It makes it feel a lot more polished as a user experience.

    I have done some example code - but not sure how to make it model (wait for a BTN1 press in Bangle 2). Somehow this could be added to the graph module or just embedded in the health app.


    3 Attachments

    • example2.png
    • example1.png
    • existing_chart.png
  • // test data 
    //var data = new Uint16Array([1023, 1020, 5300, 3178, 1500, 10600, 5700, 1008, 5090, 1010, 6100, 712]);
    
    // OR pull data from the health app
    var data = new Uint16Array(31);
    require("health").readDailySummaries(new­ Date(), h=>data[h.day]+=h.steps);
    
    const w = g.getWidth();
    const h = g.getHeight();
    
    // find the max value in the array, using a loop due to array size 
    function max(arr) {
      var m = -Infinity;
    
      for(var i=0; i< arr.length; i++)
        if(arr[i] > m) m = arr[i];
      return m;
    }
    
    // find the end of the data, the array might be for 31 days but only have 2 days in it
    function get_data_length(arr) {
      var nlen = arr.length;
      
      for(var i = arr.length - 1; i > 0 && arr[i] == 0;  i--)
        nlen--;
      
      return nlen;
    }
    
    // choose initial index that puts the last day on the end
    const len = get_data_length(data);
    var index = Math.max(len - 5, -5);
    const max_datum = max(data);  // find highest bar, for scaling
    
    function draw() {
      const bar_bot = 140;
      const bar_width = (w - 2) / 9;  // we want 9 bars, bar 5 in the centre
      var bar_top;
      var bar;
      
      g.setColor(g.theme.bg);
      g.fillRect(0,24,w,h);
      
      for (bar = 1; bar < 10; bar++) {
    
        if (bar == 5) {
          g.setFont('6x8', 2);
          g.setFontAlign(0,-1)
          g.setColor(g.theme.fg);
          g.drawString("DAY " + (index + bar) + "   " + data[index + bar - 1], g.getWidth()/2, 150);
          g.setColor("#00f");
        } else {
          g.setColor("#0ff");
        }
    
        // draw a fake 0 height bar if index is outside the bounds of the array
        if ((index + bar - 1) >= 0 && (index + bar - 1) < len) 
          bar_top = bar_bot - 100 * (data[index + bar - 1]) / max_datum;
        else
          bar_top = bar_bot;
    
        g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
        g.setColor(g.theme.fg);
        g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
      }
    }
    
    function next() {
      index = Math.min(len - 5, index + 1);
      //console.log("next="+index);
    }
    
    function prev() {
      index = Math.max(-4, index - 1);
      //console.log("prev="+index);
    }
    
    Bangle.on('swipe', dir => {
      if (dir == 1) prev(); else next();
      draw();
    });
    
    // handle switch display on by pressing BTN1
    Bangle.on('lcdPower', function(on) {
      if (on) draw();
    });
    
    draw();
    Bangle.loadWidgets();
    Bangle.drawWidgets();
    Bangle.setUI("clock");  // returns to launcher for now
    
    
  • That looks great! I'd definitely be up for adding this to the health app. I guess one question is whether the extra granularity of data displayed by the health app is useful? But the colours are great - that makes it much nicer.

    I think for now having the rendering inside the app is easiest (rather than extending the graph code). It's possible we'll be doing some more fancy stuff with it later anyway.

    To make it modal I'd say do: Bangle.setUI("updown", myFnToGoBack)

  • Ok - I will post my latest code for the health.app.js below.
    I have only tested on Bangle JS 2 for now.

    A few questions arise
    1) Regarding the retrieval of steps per day. The array that gets retrieved has a 0 at index 0.

    On Thursday 11th Nov - looks like this:

    DAY
     0  1   2    3     4     5      6     7     8     9    10
    DATA
    [0, 6, 33, 122, 2027, 7703, 11378, 7649, 4105, 3246, 5559, ....
    

    Day 11 has yet to conclude so we have 10 days of data.
    So basically the zero element is a dummy value of 0 for day data.
    When dsiplaying the chart we want to display from day 1 to last day of month.

    The hour data hour has significance for the 0 index - in that it is the first hour of the day from 00:00 - when displaying the chart we will want to display from hour 0-23.

    I think it would be more consistant to retrieve the data with the zero element always being significant - this way the chart labelling can be adjusted by an offset of 1 or 0 - rather than having to skip the first entry for day data or split into a new array starting at element 1. Hope that makes sense.

    2) Movement Data for Days is strange ?

    Basically as in the two charts below.
    Today my general movements, sitting and walking was the same as yesterday.
    Each day is getting 255 for the day - where as hour 8 this morning was 356.

    I guess what I am asking is - what is the unit of movement in an hour ?
    What is the unit for of movement for a day and how does that relate to the hours movement ?
    I am also seeing approx 90-100 movements during sleeping hours.
    Rolling over in bed should not score as movement.

    I think movement should be defined in terms of breaking the 8 steps in 8 threshold.
    So how many minutes was I actively walking in that hour ? This is a good idea as the point
    about healthy movement is that you have to stand for so many minutes per hour, feel the force of gravity on your bones etc. By breaking the step threshold we can say you were standing for that time. We should not be scoring movement when sitting on a sofa or sleeping.

    This means if you sit at a desk for an hour you score 0 BUT if you get up for 5 minutes walk to the bathroom for a toilet break you score 5. So if you stand every 20 minutes for 2 minutes you score 6 in that hour. A full hours walking scores 60. Being active for every minute of 8 hours would score 480. A full days mounting climbing for 8 hours could be made to register as 100 movement points for the day. So an average commute day is going to look around 60 minutes of activity or a 12.5 point day. This would give a good explanable baseline for activity.


    2 Attachments

    • download (1).png
    • download.png
  • I guess one question is whether the extra granularity of data displayed by the health app is useful?

    Not quite sure what you mean here. I think you mean - is the by hour display useful. Then yes I think its great to look at the day by hour and confirm or challenge the mental picture that has been built up in your mind as to how active you were across a 24 hour perioid.

  • @Gordon - here's my code - have a play with it and let me know what you think.
    I'm enjoying the Health App a lot more since I have done this.

    function getSettings() {
      return require("Storage").readJSON("health.json­",1)||{};
    }
    
    function setSettings(s) {
      require("Storage").writeJSON("health.jso­n",s);
    }
    
    function menuMain() {
      swipe_enabled = false;
      E.showMenu({
        "":{title:"Health Tracking"},
        "< Back":()=>load(),
        "Step Counting":()=>menuStepCount(),
        "Movement":()=>menuMovement(),
        "Heart Rate":()=>menuHRM(),
        "Settings":()=>menuSettings()
      });
    }
    
    function menuSettings() {
      swipe_enabled = false;
      var s=getSettings();
      E.showMenu({
        "":{title:"Health Tracking"},
        "< Back":()=>menuMain(),
        "Heart Rt":{
          value : 0|s.hrm,
          min : 0, max : 2,
          format : v=>["Off","10 mins","Always"][v],
          onchange : v => { s.hrm=v;setSettings(s); }
        }
      });
    }
    
    function menuStepCount() {
      swipe_enabled = false;
      E.showMenu({
        "":{title:"Step Counting"},
        "< Back":()=>menuMain(),
        "per hour":()=>stepsPerHour(),
        "per day":()=>stepsPerDay()
      });
    }
    
    function menuMovement() {
      swipe_enabled = false;
      E.showMenu({
        "":{title:"Movement"},
        "< Back":()=>menuMain(),
        "per hour":()=>movementPerHour(),
        "per day":()=>movementPerDay(),
      });
    }
    
    function menuHRM() {
      swipe_enabled = false;
      E.showMenu({
        "":{title:"Heart Rate"},
        "< Back":()=>menuMain(),
        "per hour":()=>hrmPerHour(),
        "per day":()=>hrmPerDay(),
      });
    }
    
    
    function stepsPerHour() {
      E.showMessage("Loading...");
      var data = new Uint16Array(24);
      require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
      console.log(data);
      g.clear(1);
      Bangle.drawWidgets();
      g.reset();
      Bangle.setUI("updown", ()=>menuStepCount());
      barChart("HOUR", data);
    }
    
    function stepsPerDay() {
      E.showMessage("Loading...");
      var data = new Uint16Array(31);
      require("health").readDailySummaries(new­ Date(), h=>data[h.day]+=h.steps);
      console.log(data);
      g.clear(1);
      Bangle.drawWidgets();
      g.reset();
      Bangle.setUI("updown", ()=>menuStepCount());
      barChart("DAY", data);
    }
    
    function hrmPerHour() {
      E.showMessage("Loading...");
      var data = new Uint16Array(24);
      var cnt = new Uint8Array(23);
      require("health").readDay(new Date(), h=>{
        data[h.hr]+=h.bpm;
        if (h.bpm) cnt[h.hr]++;
      });
      data.forEach((d,i)=>data[i] = d/cnt[i]);
      g.clear(1);
      Bangle.drawWidgets();
      g.reset();
      Bangle.setUI("updown", ()=>menuHRM());
      barChart("HOUR", data);
    }
    
    function hrmPerDay() {
      E.showMessage("Loading...");
      var data = new Uint16Array(31);
      var cnt = new Uint8Array(31);
      require("health").readDailySummaries(new­ Date(), h=>{
        data[h.day]+=h.bpm;
        if (h.bpm) cnt[h.day]++;
      });
      data.forEach((d,i)=>data[i] = d/cnt[i]);
      g.clear(1);
      Bangle.drawWidgets();
      g.reset();
      Bangle.setUI("updown", ()=>menuHRM());
      barChart("DAY", data);
    }
    
    function movementPerHour() {
      E.showMessage("Loading...");
      var data = new Uint16Array(24);
      require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
      g.clear(1);
      Bangle.drawWidgets();
      g.reset();
      Bangle.setUI("updown", ()=>menuMovement());
      barChart("HOUR", data);
    }
    
    function movementPerDay() {
      E.showMessage("Loading...");
      var data = new Uint16Array(31);
      require("health").readDailySummaries(new­ Date(), h=>data[h.day]+=h.movement);
      g.clear(1);
      Bangle.drawWidgets();
      g.reset();
      Bangle.setUI("updown", ()=>menuMovement());
      barChart("DAY", data);
    }
    
    // Bar Chart Code
    
    const w = g.getWidth();
    const h = g.getHeight();
    
    // find the max value in the array, using a loop due to array size 
    function max(arr) {
      var m = -Infinity;
    
      for(var i=0; i< arr.length; i++)
        if(arr[i] > m) m = arr[i];
      return m;
    }
    
    // find the end of the data, the array might be for 31 days but only have 2 days in it
    function get_data_length(arr) {
      var nlen = arr.length;
      
      for(var i = arr.length - 1; i > 0 && arr[i] == 0;  i--)
        nlen--;
      
      return nlen;
    }
    
    var data_len;
    var chart_index;
    var chart_max_datum;
    var chart_label;
    var chart_data;
    var swipe_enabled = false;
    
    function barChart(label, dt) {
      data_len = get_data_length(dt);
      chart_index = Math.max(data_len - 5, -5);  // choose initial index that puts the last day on the end
      chart_max_datum = max(dt);                 // find highest bar, for scaling
      chart_label = label;
      chart_data = dt;
      drawBarChart();
      swipe_enabled = true;
    }
    
    function drawBarChart() {
      const bar_bot = 140;
      const bar_width = (w - 2) / 9;  // we want 9 bars, bar 5 in the centre
      var bar_top;
      var bar;
      
      g.setColor(g.theme.bg);
      g.fillRect(0,24,w,h);
      
      for (bar = 1; bar < 10; bar++) {
        if (bar == 5) {
          g.setFont('6x8', 2);
          g.setFontAlign(0,-1)
          g.setColor(g.theme.fg);
          g.drawString(chart_label + " " + (chart_index + bar -1) + "   " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150);
          g.setColor("#00f");
        } else {
          g.setColor("#0ff");
        }
    
        // draw a fake 0 height bar if chart_index is outside the bounds of the array
        if ((chart_index + bar - 1) >= 0 && (chart_index + bar - 1) < data_len) 
          bar_top = bar_bot - 100 * (chart_data[chart_index + bar - 1]) / chart_max_datum;
        else
          bar_top = bar_bot;
    
        g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
        g.setColor(g.theme.fg);
        g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
      }
    }
    
    function next_bar() {
      chart_index = Math.min(data_len - 5, chart_index + 1);
    }
    
    function prev_bar() {
      chart_index = Math.max(-4, chart_index - 1);
    }
    
    Bangle.on('swipe', dir => {
      if (!swipe_enabled) return;
      if (dir == 1) prev_bar(); else next_bar();
      drawBarChart();
    });
    
    Bangle.loadWidgets();
    Bangle.drawWidgets();
    menuMain();
    
    
    
    
  • BTW I think the Bangle 2 Heart rate monitor is spot on.
    See charts below.
    I can see a lower heart rate at 5am when I will have been in deep sleep.
    And the average for the day looks about what I would expect, based on other smart watches.


    2 Attachments

    • download (2).png
    • download (3).png
  • Great, obviously I'm a bit busy now, but yes, the callbacks shouldn't be transmitting data that is always 0 - possibly that means that the last day of the month is not included?.

    'movement' is slightly nebulous in it being directly related to the amount of acceleration reported from the accelerometer. It's not meant to be in any particular units, but is there purely for use in sleep tracking (when I believe dips/peaks will indicate certain sleep periods) .

    However if it's always 255 for a day something is wrong. The idea is the number for the day should be the average of all entries (which are all clipped to 255 too).

    Looking at the code just now I discovered a deleted line! I'll just fix and commit this

  • Is the fix in the firmware?

  • No, it's in the health app. Should be sorted on BangleApps

  • Have done pull request.
    In the end used setWatch() and clearWatch() to get model state.
    setUI("updown") clashes with swipes.

    Would be posssible to track body temperature rasonably accurately using a Bangle JS 2 ?

  • Ok, thanks - shame about not being able to use setUI though. You're just wanting to do tap and swipe left/right? If so why not .setUI("leftright", ...)?

    You could try tracking body temp, but my gut feeling is it's not good enough because there's no direct skin contact. I guess if the HRM sensor had a temperature sensor (I'm not sure it does) that might be an option though

  • I had wondered the same before seeing @Gordon reply in another thread that it is ambient temperature, not skin temperature. My instinct is that it won't be reliable for body temperature or room/external temperature because of proximity to busy without contact (not that I can't imagine uses for it). I searched for watches with the same heart rate sensor and not enough of them advertised body temperature for me to believe it was a function of that sensor and not another they might have.

  • That said, there are loads of interesting things you can get from a PPG sensor that don't seem to be standard in devices. It depends on how much control we have over the component, but maybe we could use the photosensor to estimate temperature from IR. Ambient temp might be useful as an adjustment after all.

  • what about Module health not found in espruino
    Module health not found


    | |_ ___ ___ _ ||___ ___
    | |_ -| . | _| | | | | . |
    |
    |_| || |_|||_|_|

         |_| espruino.com
    

    2v24 (c) 2024 G.Williams

    Module health not found

  • Maybe you have uninstalled the health app?

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Easier to read charts for Health App

Posted by Avatar for HughB @HughB

Actions