Health 3.0
  • Weight
  • Exercise
Exercise

// Empty JSON of expected results
schema_results = {
  return {
    "fitbit_summary": [
      {
        "id": "1",
        "date": "2024-01-01",
        "createdTime": "2024-01-15T19:50:36.000Z",
        "steps": 0,
        "floors": 0,
        "restingHeartRate": 0,
        "marginalCalories": 0,
        "exercise_minutes": 0,
        "distance": 0,
        "date_actual": "2024-01-01",
        "month_num": 1,
        "month_chr": "January",
        "weekday_num": 2,
        "weekday_chr": "Monday"
      }
    ],
    "activities": [
      {
        "date": "2024-01-01",
        "name": null,
        "duration": 0
      }
    ]
  }
}
import { aq, op } from "@uwdata/arquero"
d3 = require("d3")

token = localStorage.getItem('ath_token')

results_raw = {
  // Provide an empty array with placeholder values until the data loads
  yield schema_results
  
  yield await fetch("https://api.andrewheiss.com/activity?start_date=2025-01-01", {
    body: "",
    headers: {
      "Authorization": `Bearer ${token}`,
      "content-type": "application/json"
    },
    method: "POST",
    mode: "cors"
  }).then(response => {
    if (!response.ok) {
        console.error(response);
        throw new Error('Network error');
      }
    return response.json();
  })
  .catch(error => {
      if (error.message === 'Network error') {
        console.error('Error with the API call:', error);
        return schema_results;
      } else {
        console.error('Some general error:', error);
        return schema_results;
      }
    });
}
daily = aq.from(results_raw.fitbit_summary)
  .derive({date: d => op.parse_date(d.date_actual)})
  // lol javascript is 0 indexed
  .derive({
    weekday_num: d => d.weekday_num - 1,
    month_num: d => d.month_num - 1
  })

totals = daily
  .rollup({
    total_minutes: d => op.sum(d.exercise_minutes),
    total_distance: d => op.sum(d.distance),
    total_steps: d => op.sum(d.steps)
  })

text_total_minutes = op.round(totals.get('total_minutes', 0)).toLocaleString()
text_total_distance = op.round(totals.get('total_distance', 0)).toLocaleString()
text_total_steps = op.round(totals.get('total_steps', 0)).toLocaleString()
goal_minutes = 10000
total_minutes = totals.get('total_minutes', 0)
pct_goal = total_minutes / goal_minutes
pct_goal_truncated = pct_goal >= 1 ? 1 : pct_goal

year_info = {
  const empty_date = new Date();
  const start_of_year = new Date(empty_date.getFullYear(), 0, 1);
  const end_of_year = new Date(empty_date.getFullYear() + 1, 0, 1);
  const year_progress = ((empty_date - start_of_year) / (end_of_year - start_of_year));

  const isLeapYear = (empty_date.getFullYear() % 4 == 0) && (empty_date.getFullYear() % 100 != 0) || (empty_date.getFullYear() % 400 == 0);
  const daysInYear = isLeapYear ? 366 : 365;

  const diff = empty_date - start_of_year;
  const oneDay = 1000 * 60 * 60 * 24;
  const day = Math.floor(diff / oneDay) + 1;
  
  const text = "Day " + day + " of " + daysInYear;

  return {
    pct_year: year_progress, 
    days_in_year: daysInYear, 
    yday: day,
    text: text
  }
}

progress_data = aq.from([
  {type: "Year complete", name: "Completed", value: year_info.pct_year, 
   label_right: `${(year_info.pct_year * 100).toFixed(2)}%`, 
   label_left: year_info.text},
  {type: "Year complete", name: "Remaining", value: 1 - year_info.pct_year},
  
  {type: "Exercise minutes", name: "Completed", value: pct_goal_truncated, 
   label_right: `${(pct_goal * 100).toFixed(2)}%`, 
   label_left: op.round(total_minutes).toLocaleString() + " of " + goal_minutes.toLocaleString() + " minutes"},
  {type: "Exercise minutes", name: "Remaining", value: 1 - pct_goal_truncated}
])

weekday_order = ["Sunday", "Monday", "Tuesday", "Wednesday", 
                 "Thursday", "Friday", "Saturday"]

month_order = ["January", "February", "March", "April", "May", "June", "July", 
               "August", "September", "October", "November", "December"]

by_weekday = daily
  .groupby("weekday_num")
  .rollup({
    total_minutes: d => op.sum(d.exercise_minutes),
    avg_minutes: d => op.mean(d.exercise_minutes)
  })

by_month = daily
  .groupby("month_num")
  .rollup({
    total_minutes: d => op.sum(d.exercise_minutes),
    avg_minutes: d => op.mean(d.exercise_minutes)
  })

mappings = {
  return {  
    "Total": { label: "Total minutes", variable: "total_minutes" },
    "Average": { label: "Average minutes", variable: "avg_minutes" }
  }
}
Plot.plot({
  color: {
    range: ["#FB9E07", "#868e96"]
  },
  x: {axis: null},
  y: {axis: null},
  marks: [
    Plot.barX(progress_data, {
      x: "value", 
      y: "type", 
      fill: "name"
    }),
    Plot.text(progress_data.filter(d => d.name == "Completed"), {
      x: 0,
      y: "type",
      text: "label_left",
      fill: "white",
      frameAnchor: "middle",
      textAnchor: "start",
      dx: 5,
      fontWeight: "bold",
      fontSize: 15,
      fontFamily: "Manrope"
    }),
    Plot.text(progress_data.filter(d => d.name == "Completed"), {
      x: 1,
      y: "type",
      text: "label_right",
      fill: "white",
      frameAnchor: "middle",
      textAnchor: "end",
      dx: -5,
      fontWeight: "bold",
      fontSize: 15,
      fontFamily: "Manrope"
    })
  ]
})

Total minutes

Steps recorded

Miles recorded

By Month
viewof month_value_to_show = Inputs.radio(["Total", "Average"], {value: "Total"})
Plot.plot({
  x: {
    label: null,
    domain: Array.from({length: 12}, (_, i) => i)
  },
  y: {
    label: mappings[month_value_to_show].label,
    grid: 4
  },
  marks: [
    Plot.ruleY([0]),
    Plot.axisX({
      ticks: 12, 
      tickFormat: Plot.formatMonth("en", "short"),
      tickSize: 0, 
      fontFamily: "Manrope"
    }),
    Plot.axisY({
      ticks: 4, 
      tickSize: 0, 
      nice: true,
      fontFamily: "Manrope"
    }),
    Plot.barY(by_month, {
      x: "month_num",
      y: mappings[month_value_to_show].variable,
      fill: "#17a2b8",
      tip: {
        format: {
          x: false,
          y: (d) => d.toFixed(0)
        },
        fontFamily: "Manrope"
      }
    })
  ]
})
By Weekday
viewof day_value_to_show = Inputs.radio(["Total", "Average"], {value: "Total"})
Plot.plot({
  x: {
    label: null,
    domain: Array.from({length: 7}, (_, i) => i)
  },
  y: {
    label: mappings[day_value_to_show].label,
    grid: 4
  },
  marks: [
    Plot.ruleY([0]),
    Plot.axisX({
      ticks: 7, 
      tickFormat: Plot.formatWeekday("en", "short"),
      tickSize: 0, 
      fontFamily: "Manrope"
    }),
    Plot.axisY({
      ticks: 4, 
      tickSize: 0, 
      nice: true,
      fontFamily: "Manrope"
    }),
    Plot.barY(by_weekday, {
      x: "weekday_num",
      y: mappings[day_value_to_show].variable,
      fill: "#A52C60",
      tip: {
        format: {
          x: false,
          y: (d) => d.toFixed(0)
        },
        fontFamily: "Manrope"
      }
    })
  ]
})