Three days a week I do morning three reps of two different bodyweight exercises1. I had been keeping a record of my reps in a spreadsheet, but I thought it would be interesting to do it so that I would have a graph that would automatically update on my website. So I decided to build my own app.
1 Earlier in the year I started to get pains in my shoulders so I changed the exercises I do to those that are not too hard on the shoulders — Australian style pull-ups using gymnast rings, and push-ups with a weighted back-pack, on push-up stands.
First I exported my existing data (from Apple Numbers) into a CSV file, then imported that using sqlite-utils from programming God Simon Willison:
I then used ChatGPT to create a little python script that allows me to update the SQLite database via a web form (see the screenshot above). I then covered the web page into an app I can keep in my dock using the inbuilt mechanism to do this in Safari. So I can just click on the icon in my dock to access it.
I then created a graph using D3, and I modified my website update script to automatically update the CSV file the graph uses so that I can have an automatically updated version of the graph on my website. And here it is!
Show the code
// Load the CSV filed3.csv("/data/daily_exercise.csv", d3.autoType).then(data => {// Transform data to calculate the average for each exercise and dateconst averagedData = d3.rollup( data.flatMap(d => [ { Date: d.Date,Exercise: d.Exercise,Value: d.Rep1,Notes: d.Notes }, { Date: d.Date,Exercise: d.Exercise,Value: d.Rep2,Notes: d.Notes }, { Date: d.Date,Exercise: d.Exercise,Value: d.Rep3,Notes: d.Notes }, ]), v => ({meanValue: d3.mean(v, d => d.Value),notes: v[0].Notes// Assume the same note applies to all points on the same date for an exercise }), d => d.Date,// Group by date d => d.Exercise// Further group by exercise );// Convert the nested rollup data back into a flat arrayconst plotData =Array.from(averagedData, ([Date, exercises]) =>Array.from(exercises, ([Exercise, { meanValue, notes }]) => ({Date, Exercise,Value: meanValue,Notes: notes })) ).flat();// Create the plotreturn Plot.plot({marks: [ Plot.line( plotData, {x:"Date",y:"Value",stroke:"Exercise",// Different line for each exercisetitle: d =>`${d.Exercise}: ${d.Value.toFixed(2)}`,// Tooltip with average value } ), Plot.dot(plotData, { x:"Date",y:"Value",stroke:"Exercise" }), Plot.text( plotData.filter(d => d.Notes),// Filter points that have notes {x:"Date",y:"Value",text:"Notes",// Display the note textdy:-10,// Offset the text slightly above the pointfill:"black",// Text color } ), ],x: {label:"Date",tickFormat:"%Y-%m-%d", },y: {label:"Average Repetitions",grid:true, },color: {scheme:"category10",// Use a color scheme for exerciseslegend:true, },height:400,width:785,style: {background:"none",// Transparent background } });});