Health 3.0
  • Weight
  • Exercise
Weight

token = localStorage.getItem('ath_token')

d3 = require('d3')

results = {
  yield undefined
  
  yield await fetch("https://api.andrewheiss.com/weight?start_date=2024-01-01&loess=true&forecast=true&forecast_start=2024-08-01", {
    body: "",
    headers: {
      "Authorization": `Bearer ${token}`,
      "content-type": "application/json"
    },
    method: "POST",
    mode: "cors"
  }).then(response => {
    if (!response.ok) {
        if(response.status === 401) {
            throw new Error('Unauthorized');
        } else {
            throw new Error('Network error');
        }
    }
    return response.json();
  })
  .then(data => ({
    ...data,
    wt: data.wt.map(d => ({
      ...d,
      timestamp: new Date(d.timestamp),
      timestamp_local_as_utc: new Date(d.timestamp_local_as_utc),
    })),
    trend: data.trend.map(d => ({
      ...d,
      timestamp_local_as_utc: new Date(d.timestamp_local_as_utc),
    })),
    forecast: data.forecast.map(d => ({
      ...d,
      timestamp_local_as_utc: new Date(d.timestamp_local_as_utc),
    }))
  }))
  .catch(error => {
      if (error.message === 'Network error') {
        console.error('Error with the API call:', error);
        return error.message;
      } else if (error.message === 'Unauthorized') {
        console.error('Unauthorized:', error);
        return error.message;
      } else {
        console.error('Some general error:', error);
        return "Error";
      }
    });
}
{
  const zoom = d3.zoom().on("zoom", handleZoom);

  function handleZoom(e) {
    const scale = plot.scale("x");
    const x = d3.scaleLinear().domain(scale.domain).range(scale.range);
    const startDate = new Date(
      Math.max(e.transform.rescaleX(x).domain()[0], extent[0].getTime())
    );

    const endDate = new Date(
      Math.min(e.transform.rescaleX(x).domain()[1], extent[1])
    );

    mutable domain = [startDate, endDate];
  }

  function initZoom() {
    d3.select(plot).call(zoom);
  }
  
  initZoom();
}
extent = {
  if (results == undefined) {
    return undefined;
  } else if (show_forecast) {
    let min = d3.min(results.wt, ({ timestamp_local_as_utc }) => timestamp_local_as_utc);
    let max = d3.max(results.forecast, ({ timestamp_local_as_utc }) => timestamp_local_as_utc);
    return [min, max];
  } else {
    return d3.extent(results.wt, ({ timestamp_local_as_utc }) => timestamp_local_as_utc)
  }
}
  
mutable domain = extent;
{
  let plotContainer = document.querySelector('.weight-plot');

  let container = document.getElementById('quarto-content');
  let loadingContainer = document.getElementById('loading-alert');

  // If the loading container doesn't exist, create it
  if (!loadingContainer) {
    container.insertAdjacentHTML('afterbegin', `
      <div id="loading-alert" class="container mt-3">
      </div>
    `);
    loadingContainer = document.getElementById('loading-alert');
  }

  if (results === undefined) {
    loadingContainer.innerHTML = `
      <div class="alert alert-info" role="alert">
          <i class="fa-solid fa-clock fa-spin"></i>&ensp;Loading data…
      </div>
    `;
    
    plotContainer.style.display = "";
  } else if (!token) {
    loadingContainer.innerHTML = `
      <div class="alert alert-warning" role="alert">
          <i class="fa-solid fa-lock"></i>&ensp;Not logged in!
      </div>
    `;
    
    plotContainer.style.display = "none";
  } else {
    loadingContainer.remove();
    plotContainer.style.display = "";
  }
}
{
  if (!token) {
  // Hide the .observablehq--error div
    let errorDiv = document.querySelector('.weight-plot .observablehq--error');
    if (errorDiv) {
      errorDiv.style.display = 'none';
    }
  }
}
Progress over time
viewof show_forecast = Inputs.toggle({label: "Forecast", value: false})
viewof show_trend_tips = Inputs.toggle({label: "Tooltips for trendline", value: false})
viewof show_thresholds = Inputs.toggle({label: "Thresholds", value: false})
plot = Plot.plot({
    style: {
      fontSize: "14px",
      fontFamily: "Manrope",
    },
    marginBottom: 40,
    marginLeft: 40,
    
    x: {
      domain,
      label: "Time",
      grid: true
    },
    y: {
      label: "Weight",
      grid: true
    },
  
    color: {
      label: "Time of day",
      domain: ["Morning", "Evening"],
      range: ["#FB9E07", "#771C6D"]
    },
  
    marks: [
      Plot.axisX({
        label: null,
        tickSize: 0, 
        nice: true
      }),
      
      Plot.axisY({
        tickSize: 0, 
        nice: true
      }),
      
      Plot.ruleY(show_thresholds ? results.bmi : [], {
          y: "weight",
          stroke: "#42159D",
          strokeDasharray: "3,2",
          channels: {"Threshold": "threshold"},
          tip: {
            format: {
              Threshold: true,
              y: (d) => d.toFixed(2)
            }
          }
        }
      ),
      
      Plot.areaY(results.trend, {
        x: "timestamp_local_as_utc", 
        y1: "conf_low", 
        y2: "conf_high",
        fillOpacity: 0.1
      }),
      Plot.line(results.trend, {
        x: "timestamp_local_as_utc", 
        y: "weight_pred",
        channels: {"Average weight": "weight_pred"},
        tip: !show_trend_tips ? false : {
          format: {
            x: (d) => d3.utcFormat("%A, %B %e at %H:%M")(d).replace(/  +/g, ' '),
            y: false,
            "Average weight": (d) => d.toFixed(2)
          }
        }
      }),
      
      Plot.areaY(show_forecast ? results.forecast : [], {
        x: "timestamp_local_as_utc", 
        y1: "conf_low", 
        y2: "conf_high",
        fill: "#A52C60",
        fillOpacity: 0.1
      }),
      Plot.line(show_forecast ? results.forecast : [], {
        x: "timestamp_local_as_utc", 
        y: "weight_pred",
        stroke: "#A52C60",
        channels: {"Predicted weight": "weight_pred"},
        tip: {
          format: {
            x: (d) => d3.utcFormat("%A, %B %e")(d).replace(/  +/g, ' '),
            y: false,
            "Predicted weight": (d) => d.toFixed(2)
          }
        }
      }),
      
      Plot.dot(results.wt, {
        x: "timestamp_local_as_utc",
        y: "weight",
        fill: "time_of_day",
        r: 5,
        tip: show_trend_tips ? false : {
          format: {
            fill: true,
            x: (d) => d3.utcFormat("%A, %B %e at %H:%M")(d).replace(/  +/g, ' '),
            y: true
          }
        }
      })
    ]
  })