Liftograph
Contact Us
If you have any questions, write to us at info@liftosaur.com!
What Liftograph is
The Graphs tab in Liftosaur shows a dashboard: a page of graphs, numbers, a muscle map and a training calendar.
Every dashboard is stored as a short piece of text, written in a small language called Liftograph.
The Edit dashboard screen also has settings forms that write the text for you. See Graphs and Dashboards. An AI assistant like ChatGPT or Claude can write a whole dashboard from a description. See Using AI with Liftosaur. Your own formulas work only in text.
Where to type it
- Open the Graphs tab.
- Tap the pencil at the top right. The Edit dashboard screen opens.
- Tap the text button at the top right. It is the second of the two buttons there.
- Type your dashboard. Tap Save when you are done.
The editor suggests what to type next. It marks mistakes and tells you what is wrong.
To add another dashboard, tap the dashboard name at the top of the Graphs tab, then New dashboard. Tap a name in the same list to switch dashboards. You can have up to 20.
A first dashboard
// ## Strength
row {
value / name: Squat e1RM / source: exercise(Squat) / y: e1rm / compare: 4w
value / name: Bench e1RM / source: exercise(Bench Press) / y: e1rm / compare: 4w
}
graph / range: 1y {
name: Squat / source: exercise(Squat) / y: e1rm
name: Deadlift / source: exercise(Deadlift) / y: e1rm
}
row {
muscles / range: 7d / show: map
calendar / range: 6m
}
From top to bottom, it shows:
- A heading, "Strength".
- A row with two big numbers side by side. Each one is your current e1RM, with the change over the last 4 weeks. e1RM means estimated one rep max: the weight you could lift for 1 rep, calculated from your sets.
- A graph of your squat and deadlift e1RM over the last year.
- A row with a body map of last week's sets, and a calendar of your workouts in the last 6 months.
The next sections explain each part: how a line works, the four widgets, rows, text, and series.
How a line works
Liftograph is written line by line. Spaces at the start of a line don't matter. Blank lines between widgets don't matter either. They only change how text works, see Text, headings and captions.
A widget is one block on the page. A widget line starts with its kind, then has settings separated by slashes (/):
graph / range: 1y
Each setting is a name, a colon, and a value: range: 1y. The order of the settings doesn't matter.
Some widgets hold more lines inside { }. A graph holds its lines there, one per line of text (see Series). A row holds the widgets that go side by side (see Rows).
Time periods
Many settings take a period of time: a number and a letter.
dfor days, like14d.wfor weeks, like8w.mfor months, like3m.yfor years, like1y.
all means your whole history.
Quotes
Names you type yourself go in double quotes when they contain one of , / ( ) : { } ". That covers exercise names, name: labels and program names. For example, a custom exercise named "Row (Chest Supported)" is written like this:
source: exercise("Row (Chest Supported)")
The brackets and commas that are part of the syntax itself, like exercise(Squat, Barbell) or max(1w), never take quotes.
To split a long line in two, end the first part with \. The next line continues it.
Widgets
There are four kinds of widget: graph, value, muscles and calendar. Text is the fifth thing you can put on a page.
graph
A graph shows one or more lines over time. Each line is a series. The series go inside { }, one per line of text:
graph / range: 6m {
name: Weight / source: exercise(Bench Press) / y: maxWeight
name: e1RM / source: exercise(Bench Press) / y: e1rm
}
range:how much time the graph shows when it opens, ending today. Without it, the graph shows all your history. On the Graphs tab, the 1M, 3M, 6M, 1Y and All buttons still switch it.
For the settings of each series, see Series.
value
A value shows one big number, such as your current e1RM or your bodyweight. A value has exactly one series, and its settings go right on the value line:
value / name: Bench e1RM / source: exercise(Bench Press) / y: e1rm / compare: 4w / trend: 3m
The number is the latest point of the series. When the series uses smooth: over a time period, the period ends today. So smooth: count(7d) on a workout source shows how many workouts you did in the last 7 days.
compare:shows how much the value changed since that long ago, like "▲ 7.5 kg vs 4w". It can be up to10y, and can't beall.trend:draws a small chart of the value over that time, under the number. It can be up to1y, and can't beall.
muscles
A muscles widget shows how many sets each muscle group got per week, as a body map, a list, or both. It has no series.
muscles / range: 4w / show: map, sets(chest, lats, quadriceps)
range:how far back to count, ending today. The numbers are always per week, sorange: 4wshows the weekly average over the last 4 weeks. Without it, the range is the last 7 days, which is also the shortest range.show:what to draw.mapis the body map.setsis the list.map, setsis both, and is what you get withoutshow:.sets(chest, lats)limits the list to those muscle groups, in that order. The names are listed in Muscle groups.source:counts only some workouts, for examplesource: workout(program: GZCLP). See Where the data comes from.
calendar
A calendar shows one square per day. A colored square is a day with a finished workout. At the top, it shows your week streak: how many weeks in a row you finished at least one workout.
calendar / range: 6m
range:how many days to show, ending today. Without it, the calendar shows the last 3 months.
Rows
A widget on its own line takes the full width of the screen. To put widgets side by side, wrap them in row { }:
row {
value / name: Squat / source: exercise(Squat) / y: e1rm
value / name: Bench / source: exercise(Bench Press) / y: e1rm
value / name: Deadlift / source: exercise(Deadlift) / y: e1rm
}
A row holds 1 to 4 items, and each item gets an equal share of the row. Each kind needs a minimum share:
valueand text need a quarter of the row. Up to 4 fit in one row.graph,musclesandcalendarneed half of the row. Up to 2 fit in one row.
Text, headings and captions
A line that starts with // is text that the app shows on the page. It can use Markdown:
**bold**and_italic_.## Headingfor a heading.- itemfor a list.[text](https://example.com)for a link.
Where you put the text changes what it does:
- Text directly above a widget or a row, with no blank line between, is its caption.
- Text followed by a blank line is a separate block of text. Inside a row, it takes a quarter of the row, like a
value.
A line that starts with /// is a note for yourself. The app does not show it.
/// My main lifts.
// **Training days, last 6 months**
calendar / range: 6m
Series
A series is the data behind one line of a graph, or behind the number of a value. A series line looks like this:
name: Squat / source: exercise(Squat) / y: e1rm / group: max(1w)
source:picks where the data comes from.y:picks the number to take from each record.group:andsmooth:shape the line. Both are optional.name:,mark:,axis:andcolor:set how the line looks.
Where the data comes from
Each source gives a list of records: one per workout with that exercise, one per workout, or one per logged measurement. Each record becomes one point on the graph.
exercise(Squat)reads every workout where you did that exercise.exercise(Squat, Barbell)picks the equipment. Without it, the exercise's default equipment is used.exercise(Squat, label: T1)reads only the squats labeled T1 in your program, liket1: Squat / 3x5.exercise(Squat, program: "5/3/1", day: 2)reads only the squats from that program, and from day 2 of it. You can use either filter on its own.workoutreads your whole workouts, one record per workout. It takes the same filters:workout(program: GZCLP, day: 1).stats(weight)reads one of your measurements, one record per entry you logged.
The exercise name must match an exercise in Liftosaur: a built-in one, or one of your custom exercises. A series for an exercise you never logged has no points, so its line stays empty. That is not a mistake in the text.
The measurements you can use in stats( ):
- Bodyweight:
weight. - Body fat percentage:
bodyfat. - Body measurements:
neck,shoulders,bicepLeft,bicepRight,forearmLeft,forearmRight,chest,waist,hips,thighLeft,thighRight,calfLeft,calfRight. - From Apple Health or Health Connect:
sleepin hours,caloriesin kcal, andproteinin grams. See Health Sync.
What number to plot
y: picks the number to take from each record:
maxWeight: the heaviest weight you lifted in that exercise in that workout. This is the default for anexercisesource.e1rm: the best estimated one rep max of that workout.volume: weight × reps, added up over all sets. This works for anexerciseor aworkout, and is the default for aworkout.maxReps: the most reps you did in one set.muscleSets(chest): how many sets the chest got in that workout. Use it with aworkoutsource.muscleVolume(chest): the volume for the chest in that workout. Use it with aworkoutsource.duration: how long the workout took, in minutes. Use it with aworkoutsource.value: the measurement itself. This is the default for astatssource, so you can leavey:out.
Shaping the line
Without shaping, a graph has one point per record. group: and smooth: change that. The app applies y: first, then group:, then smooth:.
group: joins all the points in one period into a single point. You write how to join them, then the period:
group: max(1w)keeps the best value of each week.group: sum(1w)adds up each week.group: avg(1m)takes the average of each month.group: count(1w)counts the points in each week. With aworkoutsource, that is workouts per week.
The ways to join are max, min, sum, avg, last and count. Here the period is a calendar week or month. Weeks start on the day set in Week starts from in Me → Settings.
muscleSets and muscleVolume give one point per workout. For a total per week, add group: sum(1w):
graph / range: 6m {
name: Chest sets per week / source: workout / y: muscleSets(chest) / group: sum(1w)
}
smooth: evens out a jumpy line. It keeps every point, but replaces each one with a summary of the points up to it:
smooth: avg(5)gives the average of the last 5 points.smooth: avg(7d)gives the average of the last 7 days. Use it for bodyweight, which goes up and down from day to day.smooth: count(7d)counts the points in the last 7 days.
The ways to smooth are avg, sum, count, max and min. The period can be up to 10y, and can't be all.
graph / range: 6m {
name: Weight / source: stats(weight)
name: 7-day average / source: stats(weight) / smooth: avg(7d)
}
How the line looks
name:the name in the legend. A value shows it above the number.mark:line,barsordots. The default isline. Bars always start at 0.axis:leftorright. Put a series on the right axis when its numbers are very different from the others, like bodyweight next to a squat.color:red,blue,green,orange,purpleorgray.
graph / range: 3m {
name: Protein / source: stats(protein) / group: avg(1w) / mark: bars
name: Bodyweight / source: stats(weight) / group: avg(1w) / axis: right / color: green
}
Muscle groups
These are the names to use in muscleSets( ), muscleVolume( ) and show: sets( ):
chest, lats, upperBack, frontDelts, sideDelts, rearDelts, biceps, triceps, quadriceps, hamstrings, glutes, calves, abs, upperTraps, lowerBack, neck, rotatorCuff, forearmFlexors, forearmExtensors, obliques, hipFlexors, abductors, adductors.
Your own custom muscle groups work too. muscleSets(total) and muscleVolume(total) count all muscle groups together. total does not work in show: sets( ).
Example dashboards
Copy one of these into the text editor and change it to fit your training. Replace any exercise you don't do with one you do.
Powerlifting
// ## Big three
row {
value / name: Squat / source: exercise(Squat) / y: e1rm / compare: 4w / trend: 3m
value / name: Bench / source: exercise(Bench Press) / y: e1rm / compare: 4w / trend: 3m
value / name: Deadlift / source: exercise(Deadlift) / y: e1rm / compare: 4w / trend: 3m
}
graph / range: 1y {
name: Squat / source: exercise(Squat) / y: e1rm / group: max(1w)
name: Bench / source: exercise(Bench Press) / y: e1rm / group: max(1w)
name: Deadlift / source: exercise(Deadlift) / y: e1rm / group: max(1w)
}
Hypertrophy
// ## Weekly sets
graph / range: 6m {
name: Chest / source: workout / y: muscleSets(chest) / group: sum(1w)
name: Back / source: workout / y: muscleSets(lats) / group: sum(1w)
name: Quads / source: workout / y: muscleSets(quadriceps) / group: sum(1w)
}
row {
muscles / range: 7d / show: map
calendar / range: 3m
}
graph / range: 6m {
name: Volume / source: workout / y: volume / group: sum(1w) / mark: bars
}
Body composition
row {
value / name: Bodyweight / source: stats(weight) / smooth: avg(7d) / compare: 4w / trend: 3m
value / name: Waist / source: stats(waist) / compare: 4w
}
graph / range: 6m {
name: Weight / source: stats(weight)
name: 7-day average / source: stats(weight) / smooth: avg(7d)
}
Consistency
row {
value / name: Workouts this week / source: workout / smooth: count(7d) / compare: 1w
value / name: Minutes this week / source: workout / y: duration / smooth: sum(7d) / compare: 1w
}
calendar / range: 6m
graph / range: 6m {
name: Workouts per week / source: workout / group: count(1w) / mark: bars
name: Sleep / source: stats(sleep) / group: avg(1w) / axis: right
}
A program's T1 and T2 squats
For programs like GZCLP, where the same lift appears as T1 and T2:
graph / range: 1y {
name: T1 / source: exercise(Squat, label: T1) / y: maxWeight
name: T2 / source: exercise(Squat, label: T2) / y: maxWeight
}
Common mistakes
The editor shows each mistake with the line it is on. The most common ones:
- Unknown exercise 'X'. The name doesn't match any exercise in Liftosaur. Check the spelling, or pick the exercise from the suggestions.
- Unknown stat 'X'. Use one of the measurement names listed in Where the data comes from.
- Unknown key 'X'. A setting name has a typo, or this kind of widget doesn't have that setting.
- A
y:that doesn't match the source. For example,source: stats(weight) / y: e1rm. A measurement has no e1RM. - A row holds at most 4 items. Or a widget doesn't fit in its row. A graph, muscles or calendar widget needs half of the row.
- A name contains one of
,/():{}". Put it in double quotes, likename: "Squat / BW". - An empty graph with no mistake shown. You have no records for that source yet, for example an exercise you never logged.
Each dashboard can be up to 64 KB of text.
Your own formulas
When none of the y: options gives the number you want, you can write a script instead. Scripts use the same language as program progressions, called Liftoscript. See Liftoscript for how it works.
The script goes at the end of a series line, between {~ and ~}. It runs once for every record, from oldest to newest. Inside it, point(y) adds a point with the value y.
How scripts work:
- If the script doesn't call
point()for a record, that record adds no point. So anifaroundpoint()skips records. var.variables start fresh for every record.state.variables keep their value from one record to the next, and start at 0.- Most values are lists with one entry per set.
for (var.i in completedReps)goes through the sets, andcompletedReps[var.i]reads one of them. max,min,sum,avgandcountturn a list into one number:max(e1rm)is the biggest e1RM among the sets. You can also usefloor,ceil,round,calculate1RM(weight, reps)andrpeMultiplier.point(x, y)puts a number on the bottom axis instead of a date. All series in one graph must use the same form. With numbers on the bottom axis,group:takes no period, likegroup: max, andsmooth:takes a number of points.
What you can read in an exercise record:
completedWeights,weightsande1rm: one weight per set.completedReps,reps,minReps,completedRPEandRPE: one value per set.numberOfSets,completedNumberOfSets,volume,bodyweightandrm1.day,weekanddayInWeek: where the workout was in the program.exercise: the name of the exercise.isExercise("Squat")andisProgramExercise("t1: Squat")check it.programState("rm1"): a program state variable, as it was saved with that workout.
A workout record has the same per-set values for every set of the workout. It also has duration in minutes, exercises (how many exercises), muscleSets("chest"), muscleVolume("chest"), isSetExercise(i, "Squat") and isSetProgramExercise(i, "t1: Squat").
A stats record has value and bodyweight.
When data is missing, Liftograph leaves a gap instead of using 0. Math with a gap gives a gap, and point() with a gap adds no point. For example, bodyweight is a gap before your first weigh-in.
This graph counts the sets of squats at RPE 8 or harder, per week:
graph {
name: Hard sets / source: exercise(Squat) / group: sum(1w) / mark: bars {~
var.hard = 0
for (var.i in completedRPE) {
if (completedRPE[var.i] >= 8) { var.hard += 1 }
}
point(var.hard)
~}
}
This one shows your squat e1RM divided by your bodyweight:
graph {
name: "Squat / BW" / source: exercise(Squat) / smooth: avg(5) {~ point(max(e1rm) / bodyweight) ~}
}
This one shows the heaviest weight you lifted for each number of reps:
// **Squat: best weight per rep count, last 12 months**
graph / range: 1y {
name: Squat / source: exercise(Squat) / group: max {~
for (var.i in completedReps) { point(completedReps[var.i], completedWeights[var.i]) }
~}
}