Eccentric quadriceps exercises (gold standard). Heavy slow resistance training. Stretching of quadriceps, hamstrings, and calf. Hip and core strengthening.
"patellar tendinopathy" AND "eccentric exercise" AND rehabilitation
"patellar tendinopathy" AND "heavy slow resistance"
PMID: 42192475
PMID: 41452311
Clinical implication: HSR is increasingly preferred over pure eccentric training because it is better tolerated, easier to progressively overload, and produces equivalent outcomes. Many current protocols combine both.
| Muscle | Stretch | Duration |
|---|---|---|
| Quadriceps | Standing heel-to-buttock or prone knee flexion | 3 x 30-60 sec |
| Hamstrings | Supine straight-leg raise or seated forward bend | 3 x 30-60 sec |
| Gastrocnemius | Straight-leg wall stretch | 3 x 30-60 sec |
| Soleus | Bent-knee wall stretch | 3 x 30-60 sec |
| Phase | Weeks | Focus |
|---|---|---|
| 1 - Isometric loading | 1-2 | Pain management; wall sits, isometric leg press (5 x 45 sec) - high analgesic effect |
| 2 - Isotonic loading (HSR/eccentric) | 3-12 | Progressive loading per above; decline squats and/or HSR |
| 3 - Energy storage | 8-16 | Plyometrics, sport-specific loading (hopping, bounding) |
| 4 - Return to sport | 12-24 | Gradual reintroduction; sport-specific drills, maintenance loading |
Create a 12-week patellar tendinopathy rehab tracker with exercise prescription sheets
mkdir -p /home/daytona/workspace/patellar-rehab && ls /home/daytona/workspace/pa…
~/patellar-rehab/build_tracker.py
import openpyxl
from openpyxl.styles import (
Font, PatternFill, Alignment, Border, Side, GradientFill
)
from openpyxl.utils import get_column_letter
from openpyxl.chart import BarChart, LineChart, Reference
from openpyxl.chart.series import SeriesLabel
from openpyxl.worksheet.datavalidation import DataValidation
import openpyxl.drawing.image
wb = openpyxl.Workbook()
# ─── Color Palette ───────────────────────────────────────────────────────────
C_NAVY = "1B3A6B" # dark navy (headers)
C_TEAL = "2E86AB" # mid teal (sub-headers)
C_SKYBLUE = "A8DADC" # light blue (row highlights)
C_ORANGE = "E76F51" # accent orange (warnings / important)
C_GREEN = "52B788" # green (progress / complete)
C_YELLOW = "FFD166" # yellow (input cells)
C_LTGRAY = "F4F4F4" # light gray (alternate rows)
C_WHITE = "FFFFFF"
C_DARKTEXT = "1A1A2E"
C_PHASE1 = "D4E8F0" # phase 1 bg
C_PHASE2 = "D4EDD4" # phase 2 bg
C_PHASE3 = "FFF3CD" # phase 3 bg
C_PHASE4 = "FDDCDC" # phase 4 bg
def fill(hex_color):
return PatternFill("solid", fgColor=hex_color)
def font(bold=False, size=11, color=C_DARKTEXT, italic=False):
return Font(name="Arial", bold=bold, size=size, color=color, italic=italic)
def center():
return Alignment(horizontal="center", vertical="center", wrap_text=True)
def left():
return Alignment(horizontal="left", vertical="center", wrap_text=True)
def thin_border():
s = Side(style="thin", color="CCCCCC")
return Border(left=s, right=s, top=s, bottom=s)
def thick_border():
s = Side(style="medium", color=C_NAVY)
return Border(left=s, right=s, top=s, bottom=s)
def apply_header(ws, row, col, text, bg=C_NAVY, fg=C_WHITE, sz=12, bold=True,
colspan=1, align="center"):
cell = ws.cell(row=row, column=col, value=text)
cell.font = Font(name="Arial", bold=bold, size=sz, color=fg)
cell.fill = fill(bg)
cell.alignment = Alignment(horizontal=align, vertical="center", wrap_text=True)
cell.border = thin_border()
if colspan > 1:
ws.merge_cells(
start_row=row, start_column=col,
end_row=row, end_column=col + colspan - 1
)
def set_col_widths(ws, widths_dict):
for col_letter, width in widths_dict.items():
ws.column_dimensions[col_letter].width = width
def alt_row_fill(ws, row, cols, even_row, color1=C_WHITE, color2=C_LTGRAY):
bg = color2 if even_row else color1
for c in range(1, cols + 1):
cell = ws.cell(row=row, column=c)
if not cell.fill or cell.fill.fgColor.rgb in ("00000000", C_WHITE, "FFFFFFFF"):
cell.fill = fill(bg)
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 1 — COVER
# ═════════════════════════════════════════════════════════════════════════════
ws1 = wb.active
ws1.title = "Cover"
ws1.sheet_view.showGridLines = False
ws1.row_dimensions[1].height = 10
ws1.row_dimensions[2].height = 60
ws1.row_dimensions[3].height = 30
ws1.row_dimensions[4].height = 18
# Title banner
ws1.merge_cells("A2:H2")
c = ws1["A2"]
c.value = "PATELLAR TENDINOPATHY | 12-WEEK REHABILITATION TRACKER"
c.font = Font(name="Arial", bold=True, size=20, color=C_WHITE)
c.fill = fill(C_NAVY)
c.alignment = Alignment(horizontal="center", vertical="center")
ws1.merge_cells("A3:H3")
c = ws1["A3"]
c.value = "Evidence-Based Conservative Management Protocol"
c.font = Font(name="Arial", italic=True, size=13, color=C_WHITE)
c.fill = fill(C_TEAL)
c.alignment = Alignment(horizontal="center", vertical="center")
# Patient info section
info_start = 5
ws1.merge_cells(f"A{info_start}:H{info_start}")
c = ws1.cell(row=info_start, column=1, value="PATIENT INFORMATION")
c.font = Font(name="Arial", bold=True, size=12, color=C_WHITE)
c.fill = fill(C_NAVY)
c.alignment = Alignment(horizontal="left", vertical="center")
c.border = thin_border()
ws1.row_dimensions[info_start].height = 22
fields = [
("Patient Name:", "", "Date of Birth:", ""),
("Clinician / Physio:", "", "Start Date:", ""),
("Dominant Leg:", "", "Affected Side:", ""),
("Diagnosis Confirmed By:", "", "Imaging (US/MRI):", ""),
("VISA-P Score (Baseline):", "", "Pain NRS (Baseline):", ""),
("Short-Term Goal:", "", "Long-Term Goal:", ""),
]
for i, (l1, v1, l2, v2) in enumerate(fields):
r = info_start + 1 + i
ws1.row_dimensions[r].height = 20
for col, val, is_label in [(1, l1, True),(2, v1, False),(5, l2, True),(6, v2, False)]:
cell = ws1.cell(row=r, column=col, value=val)
cell.font = Font(name="Arial", bold=is_label, size=11,
color=C_DARKTEXT if is_label else C_NAVY)
cell.fill = fill(C_LTGRAY if is_label else C_YELLOW)
cell.alignment = left()
cell.border = thin_border()
ws1.merge_cells(start_row=r, start_column=2, end_row=r, end_column=4)
ws1.merge_cells(start_row=r, start_column=6, end_row=r, end_column=8)
# Protocol overview box
pr = info_start + len(fields) + 2
ws1.merge_cells(f"A{pr}:H{pr}")
c = ws1.cell(row=pr, column=1, value="PROTOCOL OVERVIEW")
c.font = Font(name="Arial", bold=True, size=12, color=C_WHITE)
c.fill = fill(C_TEAL)
c.alignment = Alignment(horizontal="left", vertical="center")
ws1.row_dimensions[pr].height = 22
phases = [
("PHASE 1", "Weeks 1-2", "Isometric Loading", "Pain modulation; tendon activation", C_PHASE1),
("PHASE 2", "Weeks 3-6", "Heavy Slow Resistance (HSR)", "Tendon loading & strength building", C_PHASE2),
("PHASE 3", "Weeks 7-10", "Eccentric Overload + HSR", "Collagen remodeling; progressive overload", C_PHASE3),
("PHASE 4", "Weeks 11-12", "Energy Storage / Return to Sport Prep", "Plyometrics; sport-specific loading", C_PHASE4),
]
headers = ["Phase", "Weeks", "Focus", "Goal", ""]
apply_header(ws1, pr+1, 1, "Phase", bg=C_NAVY, sz=11)
apply_header(ws1, pr+1, 2, "Weeks", bg=C_NAVY, sz=11)
apply_header(ws1, pr+1, 3, "Focus", bg=C_NAVY, sz=11, colspan=3)
apply_header(ws1, pr+1, 6, "Goal", bg=C_NAVY, sz=11, colspan=3)
ws1.row_dimensions[pr+1].height = 20
for i, (ph, wk, focus, goal, bg_col) in enumerate(phases):
r = pr + 2 + i
ws1.row_dimensions[r].height = 22
for col, val in [(1, ph),(2, wk)]:
cell = ws1.cell(row=r, column=col, value=val)
cell.font = Font(name="Arial", bold=True, size=11, color=C_DARKTEXT)
cell.fill = fill(bg_col)
cell.alignment = center()
cell.border = thin_border()
ws1.merge_cells(start_row=r, start_column=3, end_row=r, end_column=5)
c = ws1.cell(row=r, column=3, value=focus)
c.font = Font(name="Arial", size=11, color=C_DARKTEXT)
c.fill = fill(bg_col)
c.alignment = left()
c.border = thin_border()
ws1.merge_cells(start_row=r, start_column=6, end_row=r, end_column=8)
c = ws1.cell(row=r, column=6, value=goal)
c.font = Font(name="Arial", size=11, color=C_DARKTEXT)
c.fill = fill(bg_col)
c.alignment = left()
c.border = thin_border()
# Legend
lr = pr + len(phases) + 3
ws1.merge_cells(f"A{lr}:H{lr}")
c = ws1.cell(row=lr, column=1,
value="INPUT CELLS (yellow) are for patient/clinician data entry. All other cells are locked reference content.")
c.font = Font(name="Arial", italic=True, size=10, color="888888")
c.alignment = left()
# Note
nr = lr + 1
ws1.merge_cells(f"A{nr}:H{nr}")
c = ws1.cell(row=nr, column=1,
value="Reference: Liu et al. 2026 (BMC Sports Sci Med Rehabil) | Hjortshoej et al. 2025 (Scand J Med Sci Sports) | Rosen's EM | Miller's Orthopaedics")
c.font = Font(name="Arial", italic=True, size=9, color="AAAAAA")
c.alignment = left()
# Tabs guide
tabs_r = nr + 2
ws1.merge_cells(f"A{tabs_r}:H{tabs_r}")
c = ws1.cell(row=tabs_r, column=1, value="WORKBOOK TABS")
c.font = Font(name="Arial", bold=True, size=12, color=C_WHITE)
c.fill = fill(C_ORANGE)
c.alignment = left()
ws1.row_dimensions[tabs_r].height = 22
tabs = [
("Cover", "This sheet — patient info and protocol overview"),
("Phase Overview", "All 4 phases at a glance with progression rules"),
("Rx Phase 1 — Isometric", "Detailed exercise prescription: weeks 1-2"),
("Rx Phase 2 — HSR", "Detailed exercise prescription: weeks 3-6"),
("Rx Phase 3 — Eccentric+HSR", "Detailed exercise prescription: weeks 7-10"),
("Rx Phase 4 — Return to Sport", "Detailed exercise prescription: weeks 11-12"),
("Stretching & Hip-Core", "Full stretching and hip/core strengthening protocol"),
("12-Week Session Tracker", "Log every session: pain, load, sets, reps, notes"),
("Progress Dashboard", "Auto-calculated charts: VISA-P trend and pain trend"),
]
for i, (tab, desc) in enumerate(tabs):
r = tabs_r + 1 + i
ws1.row_dimensions[r].height = 18
c1 = ws1.cell(row=r, column=1, value=tab)
c1.font = Font(name="Arial", bold=True, size=11, color=C_TEAL)
c1.fill = fill(C_LTGRAY if i % 2 == 0 else C_WHITE)
c1.alignment = left()
c1.border = thin_border()
ws1.merge_cells(start_row=r, start_column=2, end_row=r, end_column=8)
c2 = ws1.cell(row=r, column=2, value=desc)
c2.font = Font(name="Arial", size=11, color=C_DARKTEXT)
c2.fill = fill(C_LTGRAY if i % 2 == 0 else C_WHITE)
c2.alignment = left()
c2.border = thin_border()
set_col_widths(ws1, {"A":18,"B":22,"C":22,"D":22,"E":14,"F":22,"G":22,"H":18})
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 2 — PHASE OVERVIEW
# ═════════════════════════════════════════════════════════════════════════════
ws2 = wb.create_sheet("Phase Overview")
ws2.sheet_view.showGridLines = False
# Title
ws2.merge_cells("A1:I1")
c = ws2["A1"]
c.value = "12-WEEK REHABILITATION PHASE OVERVIEW — Patellar Tendinopathy"
c.font = Font(name="Arial", bold=True, size=16, color=C_WHITE)
c.fill = fill(C_NAVY)
c.alignment = center()
ws2.row_dimensions[1].height = 36
# Phase detail table
phase_details = [
# (phase, weeks, sessions/wk, exercises, sets_reps, load, progression_rule, pain_limit, color)
(
"Phase 1\nIsometric Loading",
"1 - 2",
"Daily (5-7x/week)",
"Wall sit\nIsometric leg press\nIsometric single-leg extension",
"5 sets x 45 sec hold",
"Bodyweight / light load on leg press",
"Increase hold duration to 60 sec in week 2\nProgress to Phase 2 if pain <3/10 on SLDS",
"Pain DURING exercise: up to 5/10 allowed\nPain AFTER exercise: must return to baseline within 24h",
C_PHASE1
),
(
"Phase 2\nHeavy Slow Resistance",
"3 - 6",
"3x/week (M-W-F)",
"Leg press (bilateral)\nHack squat / Goblet squat\nKnee extension machine\nRomanian deadlift",
"Week 3-4: 4x15 (15RM)\nWeek 5-6: 4x12 (12RM)",
"70-75% 1RM\nTempo: 3 sec down / 2 sec up",
"Increase load 5-10% when reps feel easy\nProgress to Phase 3 if pain <3/10 at 12RM",
"Pain DURING: up to 5/10\nPain NEXT DAY: must be ≤ baseline",
C_PHASE2
),
(
"Phase 3\nEccentric Overload + HSR",
"7 - 10",
"3x/week (M-W-F)",
"Single-leg decline squat (25° board)\nSingle-leg leg press\nNordic hamstring curl\nStep-down",
"Week 7-8: 4x10 (10RM)\nWeek 9-10: 4x8 (8RM)\nDecline squat: 3x15 eccentric",
"80-85% 1RM for HSR\nDecline squat: add weight when pain-free",
"Add 2.5-5kg/week to decline squat\nProgress to Phase 4 if VISA-P ≥ 70",
"Pain DURING: up to 5/10\nNO increase in resting pain",
C_PHASE3
),
(
"Phase 4\nReturn to Sport Prep",
"11 - 12",
"3-4x/week",
"Plyometrics: double-leg hop, single-leg hop\nBound & stick\nDepth jump (week 12 only)\nSport-specific drills",
"Week 11: 3x10 DL hop\nWeek 12: 3x8 SL hop + depth jump 3x6",
"Bodyweight initially\nAdd resistance when pain-free",
"Full return to sport ONLY if:\n- VISA-P ≥ 80\n- Pain <2/10 on SLDS\n- Limb symmetry ≥ 90%",
"STOP if pain >5/10\nReturn to Phase 3 if pain flares",
C_PHASE4
),
]
col_headers = ["Phase","Weeks","Sessions/Week","Exercises","Sets & Reps","Load / Tempo",
"Progression Rule","Pain Tolerance",""]
col_widths = {"A":18,"B":9,"C":16,"D":24,"E":24,"F":24,"G":32,"H":32,"I":5}
# Header row
ws2.row_dimensions[2].height = 10
ws2.row_dimensions[3].height = 26
for col, hdr in enumerate(col_headers, 1):
apply_header(ws2, 3, col, hdr, bg=C_TEAL, sz=11)
# Data rows
for i, row_data in enumerate(phase_details):
r = 4 + i * 2
ws2.row_dimensions[r].height = 90
ws2.row_dimensions[r+1].height = 4
bg_c = row_data[8]
for col, val in enumerate(row_data[:8], 1):
cell = ws2.cell(row=r, column=col, value=val)
cell.font = Font(name="Arial", size=10, bold=(col == 1), color=C_DARKTEXT)
cell.fill = fill(bg_c)
cell.alignment = Alignment(horizontal="left" if col > 1 else "center",
vertical="top", wrap_text=True)
cell.border = thin_border()
# Phase progression flowchart note
nr = 4 + len(phase_details) * 2 + 1
ws2.merge_cells(f"A{nr}:I{nr}")
c = ws2.cell(row=nr, column=1,
value="PROGRESSION RULE: Move to the next phase only when pain criteria are met. If pain exceeds limits, "
"reduce load by 20% and retry after 3 sessions. Do not rush progression — tendon adaptation lags "
"behind muscle strength gains by 4-6 weeks.")
c.font = Font(name="Arial", italic=True, size=10, color=C_ORANGE)
c.fill = fill("FFF8F0")
c.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
c.border = Border(left=Side(style="medium", color=C_ORANGE),
right=Side(style="medium", color=C_ORANGE),
top=Side(style="medium", color=C_ORANGE),
bottom=Side(style="medium", color=C_ORANGE))
ws2.row_dimensions[nr].height = 50
set_col_widths(ws2, col_widths)
# ═════════════════════════════════════════════════════════════════════════════
# HELPER: Build an Exercise Prescription Sheet
# ═════════════════════════════════════════════════════════════════════════════
def build_rx_sheet(wb, title, subtitle, week_range, phase_color, exercises):
"""
exercises: list of dicts with keys:
name, description, sets, reps, tempo, load, cues, progression, image_note
"""
ws = wb.create_sheet(title)
ws.sheet_view.showGridLines = False
# Title
ws.merge_cells("A1:J1")
c = ws["A1"]
c.value = f"EXERCISE PRESCRIPTION — {subtitle}"
c.font = Font(name="Arial", bold=True, size=15, color=C_WHITE)
c.fill = fill(C_NAVY)
c.alignment = center()
ws.row_dimensions[1].height = 36
ws.merge_cells("A2:J2")
c = ws["A2"]
c.value = f"Patellar Tendinopathy | {week_range}"
c.font = Font(name="Arial", italic=True, size=11, color=C_WHITE)
c.fill = fill(C_TEAL)
c.alignment = center()
ws.row_dimensions[2].height = 20
ws.row_dimensions[3].height = 8
# Column headers
col_hdrs = ["#","Exercise","Description / Execution",
"Sets","Reps / Duration","Tempo","Load","Key Cues","Progression Trigger","Notes"]
ws.row_dimensions[4].height = 28
for col, hdr in enumerate(col_hdrs, 1):
apply_header(ws, 4, col, hdr, bg=C_TEAL, sz=11)
for i, ex in enumerate(exercises):
r = 5 + i
ws.row_dimensions[r].height = 65
bg = phase_color if i % 2 == 0 else C_WHITE
data_row = [
i+1,
ex["name"],
ex["description"],
ex["sets"],
ex["reps"],
ex["tempo"],
ex["load"],
ex["cues"],
ex["progression"],
ex.get("notes",""),
]
for col, val in enumerate(data_row, 1):
cell = ws.cell(row=r, column=col, value=val)
cell.font = Font(name="Arial", bold=(col==2), size=10,
color=C_NAVY if col==2 else C_DARKTEXT)
cell.fill = fill(bg)
cell.alignment = Alignment(
horizontal="center" if col in (1,4,5,6) else "left",
vertical="top", wrap_text=True)
cell.border = thin_border()
# Footer
fr = 5 + len(exercises) + 1
ws.merge_cells(f"A{fr}:J{fr}")
c = ws.cell(row=fr, column=1,
value="Pain monitoring: Stop if pain exceeds 5/10 during exercise. "
"Post-exercise soreness >24h = reduce load next session by 20%.")
c.font = Font(name="Arial", italic=True, size=10, color=C_ORANGE)
c.alignment = left()
c.fill = fill("FFF8F0")
c.border = Border(left=Side(style="medium", color=C_ORANGE),
bottom=Side(style="medium", color=C_ORANGE),
top=Side(style="medium", color=C_ORANGE),
right=Side(style="medium", color=C_ORANGE))
ws.row_dimensions[fr].height = 30
set_col_widths(ws, {"A":5,"B":20,"C":34,"D":7,"E":14,"F":14,"G":18,"H":30,"I":26,"J":20})
return ws
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 3 — Rx Phase 1: ISOMETRIC
# ═════════════════════════════════════════════════════════════════════════════
ex_p1 = [
{
"name": "Wall Sit (Bilateral)",
"description": "Stand with back flat against wall. Slide down until knees at ~70° flexion. Hold position statically. Keep feet hip-width apart, toes forward.",
"sets": "5",
"reps": "45 sec hold\n(→ 60 sec wk 2)",
"tempo": "Static hold",
"load": "Bodyweight\nAdd vest if pain <2/10",
"cues": "- Knees over 2nd toe\n- No bouncing / movement\n- Breathe normally\n- Quad squeeze throughout",
"progression": "Progress when pain <3/10\nduring all 5 sets",
"notes": "2 min rest between sets\nPerform morning + evening"
},
{
"name": "Isometric Leg Press",
"description": "Sit in leg press machine. Push plate to mid-range position. Hold without locking knee. Apply submaximal force (70% effort) against the immovable plate.",
"sets": "5",
"reps": "45 sec hold",
"tempo": "Static hold",
"load": "Light to moderate plate load (~30-50% BW)",
"cues": "- 70° knee flexion\n- Equal pressure both legs\n- No knee valgus\n- Consistent pressure throughout",
"progression": "Increase load by 10-20% when\nall sets pain-free",
"notes": "Strong analgesic effect — often reduces tendon pain immediately\nCan be done 2x daily"
},
{
"name": "Isometric Single-Leg Extension (Seated)",
"description": "Sit on edge of chair or extension machine. Extend knee to ~30° short of full extension. Hold. Do NOT lock knee fully.",
"sets": "5",
"reps": "45 sec hold",
"tempo": "Static hold",
"load": "Ankle weight or machine resistance",
"cues": "- Stop 30° from full extension\n- Avoid painful range\n- Keep thigh flat on surface\n- Controlled breathing",
"progression": "Perform on affected leg only;\nprogress when NRS <3/10",
"notes": "Only if leg press unavailable\nAlt for home use"
},
{
"name": "Isometric Step Hold",
"description": "Stand on a 10-15cm step, affected leg only. Slowly lower to mid-range (30° knee flexion). Hold position. Maintain upright trunk.",
"sets": "4",
"reps": "30-45 sec hold",
"tempo": "Static hold",
"load": "Bodyweight",
"cues": "- Hip-knee-toe alignment\n- No trunk lean\n- Arms by side or lightly touch wall for balance\n- Quad dominant — not hip hinge",
"progression": "Increase step height or hold duration\nonce pain-free for 3 sessions",
"notes": "Excellent for monitoring: pain on SLDS during this exercise predicts tolerance to Phase 2"
},
]
build_rx_sheet(wb, "Rx Phase 1 — Isometric",
"PHASE 1: ISOMETRIC LOADING", "Weeks 1-2", C_PHASE1, ex_p1)
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 4 — Rx Phase 2: HSR
# ═════════════════════════════════════════════════════════════════════════════
ex_p2 = [
{
"name": "Bilateral Leg Press",
"description": "Feet hip-width on platform. Lower platform to 90° knee flexion, press back to near-full extension. Control the descent (3 sec down). Both legs equally loaded.",
"sets": "4",
"reps": "Wk3-4: 15 reps\nWk5-6: 12 reps",
"tempo": "3 sec down\n1 sec pause\n2 sec up",
"load": "Wk3-4: 15RM load\nWk5-6: 12RM load (~75% 1RM)",
"cues": "- Heels on platform\n- No knee collapse\n- Full ROM\n- Breathe: exhale on press",
"progression": "Increase load 5% when\nlast set feels <8/10 RPE",
"notes": "Primary HSR exercise\nCore of phase 2"
},
{
"name": "Hack Squat / Goblet Squat",
"description": "Hack squat: feet shoulder-width on platform. Goblet squat (home): hold dumbbell/kettlebell at chest. Squat to parallel or below. Slow controlled descent.",
"sets": "4",
"reps": "Wk3-4: 15 reps\nWk5-6: 12 reps",
"tempo": "3 sec down\n2 sec up",
"load": "Wk3-4: 15RM\nWk5-6: 12RM",
"cues": "- Chest up, neutral spine\n- Knees track over toes\n- Drive through heels on ascent\n- Controlled descent",
"progression": "Add 5-10kg plates once\nall 4 sets completed pain-free",
"notes": "Goblet squat requires no\nmachine — good home option"
},
{
"name": "Knee Extension Machine",
"description": "Adjust seat so knee joint aligns with machine pivot. Extend knee from 90° to full extension. Slowly lower back. Keep thigh pressed into pad.",
"sets": "3",
"reps": "Wk3-4: 15 reps\nWk5-6: 12 reps",
"tempo": "2 sec up\n3 sec down",
"load": "Moderate (~65-70% 1RM)",
"cues": "- Full ROM extension\n- No compensatory hip shift\n- Controlled eccentric phase\n- Do NOT swing leg",
"progression": "Increase stack by 5lbs/session\nif NRS <3/10",
"notes": "Use with caution if pain\nat terminal extension;\nadjust ROM accordingly"
},
{
"name": "Romanian Deadlift (RDL)",
"description": "Hold barbell or dumbbells at hip. Hinge at hip, maintaining neutral spine, lowering weight along thighs until mild hamstring stretch. Drive hips forward to return.",
"sets": "4",
"reps": "12-15 reps",
"tempo": "3 sec down\n2 sec up",
"load": "Moderate to heavy (15RM load)",
"cues": "- Neutral spine throughout\n- Slight knee bend\n- Weight stays close to body\n- Glutes squeeze at top",
"progression": "Add 5kg when all sets\ncompleted with good form",
"notes": "Posterior chain loading\nreduces anterior knee stress"
},
{
"name": "Hip Thrust (Barbell or Bodyweight)",
"description": "Shoulders on bench, feet flat, barbell across hips. Push hips up to form table-top position. Lower slowly. Focus on glute contraction.",
"sets": "3",
"reps": "15 reps",
"tempo": "2 sec up\n1 sec hold\n3 sec down",
"load": "BW → add barbell load progressively",
"cues": "- Chin tucked\n- Neutral pelvis at top\n- Drive through heels\n- Avoid hyperextension",
"progression": "Add 10kg when form is perfect\nand no pain",
"notes": "Key hip strengthening exercise\nfor patellar tendon offloading"
},
]
build_rx_sheet(wb, "Rx Phase 2 — HSR",
"PHASE 2: HEAVY SLOW RESISTANCE", "Weeks 3-6", C_PHASE2, ex_p2)
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 5 — Rx Phase 3: Eccentric + HSR
# ═════════════════════════════════════════════════════════════════════════════
ex_p3 = [
{
"name": "Single-Leg Decline Squat (SLDS)",
"description": "Stand on 25° decline board, affected leg only. Slowly lower into single-leg squat to ~60° knee flexion. Return using BOTH legs. Repeat eccentric only on single leg.",
"sets": "3",
"reps": "15 reps\n(eccentric only on\naffected leg)",
"tempo": "4 sec down\n(eccentric)\nBilateral up",
"load": "BW first;\nadd vest/backpack\nonce pain <3/10",
"cues": "- Slow controlled descent\n- Knee tracks over 2nd toe\n- Upright trunk\n- Push through pain up to 5/10",
"progression": "Add 2.5-5kg/week once\nall 3 sets completed",
"notes": "GOLD STANDARD exercise\nMost evidence-based\nExact protocol: Alfredson/Purdam"
},
{
"name": "Single-Leg Leg Press",
"description": "Same as bilateral leg press but single leg. Foot centered on platform. Full ROM. Use uninvolved leg only to return weight.",
"sets": "4",
"reps": "Wk7-8: 10 reps\nWk9-10: 8 reps",
"tempo": "3 sec down\n2 sec up",
"load": "Wk7-8: 10RM\nWk9-10: 8RM (~80-85% 1RM)",
"cues": "- Same cues as bilateral\n- Note any asymmetry\n- Match load to BW/contralateral",
"progression": "Increase load 5% when last\nset RPE <8",
"notes": "Tests and builds side-to-side\nstrength symmetry (target ≥90%)"
},
{
"name": "Step-Down (Eccentric)",
"description": "Stand on 20cm step. Slowly lower uninvolved foot toward floor (eccentric quad loading on standing leg). Tap floor gently and return. Focus on controlling descent.",
"sets": "3",
"reps": "10-12 reps per leg",
"tempo": "4 sec down\n2 sec up",
"load": "Bodyweight\n→ add weight vest",
"cues": "- Hip-knee-ankle alignment\n- No trunk lean forward\n- Maintain level pelvis\n- Controlled, deliberate movement",
"progression": "Increase step height 5cm\nonce pain-free",
"notes": "Functional eccentric loading;\nclose to sport biomechanics"
},
{
"name": "Nordic Hamstring Curl",
"description": "Kneel on pad (feet anchored under barbell or partner holds ankles). Lower body toward ground by slowly straightening knees. Catch with hands, push back. Hamstring emphasis.",
"sets": "3",
"reps": "Wk7-8: 6 reps\nWk9-10: 8 reps",
"tempo": "4-5 sec lowering phase",
"load": "Bodyweight",
"cues": "- Hips extended\n- Neutral spine\n- Lower as slowly as possible\n- Arms catch body at floor",
"progression": "Increase reps by 1-2\nper session when RFD improves",
"notes": "Builds posterior chain\nReduced anterior knee shear force"
},
{
"name": "Bulgarian Split Squat",
"description": "Rear foot elevated on bench (45cm). Front foot forward. Lower into single-leg squat. Primarily quad-loaded exercise with hip flexor stretch.",
"sets": "4",
"reps": "Wk7-8: 10 reps\nWk9-10: 8 reps",
"tempo": "3 sec down\n2 sec up",
"load": "Dumbbells at sides\n(10RM → 8RM)",
"cues": "- Front shin slightly forward\n- Knee tracks over toe\n- Upright torso\n- Full ROM",
"progression": "Add 2.5-5kg when\nform is maintained",
"notes": "Excellent single-leg loading;\nexposes asymmetries"
},
]
build_rx_sheet(wb, "Rx Phase 3 — Eccentric+HSR",
"PHASE 3: ECCENTRIC OVERLOAD + HSR", "Weeks 7-10", C_PHASE3, ex_p3)
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 6 — Rx Phase 4: Return to Sport
# ═════════════════════════════════════════════════════════════════════════════
ex_p4 = [
{
"name": "Double-Leg Countermovement Jump (CMJ)",
"description": "Stand feet hip-width. Countermovement (quick dip), then jump maximally. Land softly with knees bent absorbing force. Focus on controlled landing.",
"sets": "3",
"reps": "Wk11: 10 reps\nWk12: 10 reps",
"tempo": "Explosive up\nSoft landing",
"load": "Bodyweight",
"cues": "- Quiet landing\n- Knee over toe on landing\n- Equal weight both legs\n- 30 sec between reps",
"progression": "Add single-leg variant once\ndouble-leg pain-free",
"notes": "Entry-level plyometric\nMonitor for pain flare"
},
{
"name": "Single-Leg Hop & Stick",
"description": "Hop forward on one leg, land and HOLD for 2 seconds. Assess stability and pain on landing. Perform on both legs and compare.",
"sets": "3",
"reps": "Wk11: 8 reps\nWk12: 8 reps",
"tempo": "Explosive hop\n2 sec stick",
"load": "Bodyweight",
"cues": "- Stick the landing\n- Knee alignment on landing\n- No trunk compensation\n- Assess pain on EACH rep",
"progression": "Progress to repeated hops\nwhen stick is controlled and pain-free",
"notes": "Key limb symmetry test:\ncompare distance both sides\n(target ≥ 90% symmetry)"
},
{
"name": "Depth Jump",
"description": "Step off box (20cm). Land, then immediately jump up (minimize ground contact time). Advanced plyometric — reactive tendon loading.",
"sets": "3",
"reps": "Wk12 only: 6 reps",
"tempo": "Minimal ground\ncontact time",
"load": "Bodyweight\n20cm box",
"cues": "- Brief ground contact\n- Immediate rebound\n- Soft knees on landing\n- Forward lean minimized",
"progression": "Increase box height to 30cm\nonce >3 sessions pain-free",
"notes": "Week 12 ONLY\nHigh tendon load — use carefully"
},
{
"name": "Lateral Bound",
"description": "Push off one leg laterally, land on opposite single leg. Hold. Tests frontal plane control and lateral power, replicating court sport demands.",
"sets": "3",
"reps": "8 per side",
"tempo": "Explosive push\nControlled landing",
"load": "Bodyweight",
"cues": "- Drive hip abductors\n- Land toe-heel\n- No knee cave\n- Hold 2 sec on landing",
"progression": "Increase distance\nor add weight vest",
"notes": "Key for basketball/volleyball\nplayers returning to sport"
},
{
"name": "Sport-Specific Running / Sprints",
"description": "Progressive running program: jog → run → sprint. Start at 50% max speed, increase 10% per session if pain-free. Include cutting and change-of-direction.",
"sets": "3-4",
"reps": "30-40m per rep\n(gradual speed increase)",
"tempo": "Submaximal\n(build to 90%+)",
"load": "Bodyweight",
"cues": "- Monitor pain during AND after\n- Stop if pain >4/10\n- Allow full recovery between reps",
"progression": "Full return to sport when\nVISA-P ≥80 + LSI ≥90%\n+ NRS <2 on SLDS",
"notes": "Final clearance requires:\nVISA-P ≥80\nPain <2/10 on SLDS\nLimb Symmetry Index ≥90%"
},
]
build_rx_sheet(wb, "Rx Phase 4 — Return to Sport",
"PHASE 4: ENERGY STORAGE & RETURN TO SPORT", "Weeks 11-12", C_PHASE4, ex_p4)
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 7 — Stretching & Hip-Core
# ═════════════════════════════════════════════════════════════════════════════
ws7 = wb.create_sheet("Stretching & Hip-Core")
ws7.sheet_view.showGridLines = False
ws7.merge_cells("A1:I1")
c = ws7["A1"]
c.value = "STRETCHING & HIP / CORE STRENGTHENING PROTOCOL — All 12 Weeks"
c.font = Font(name="Arial", bold=True, size=15, color=C_WHITE)
c.fill = fill(C_NAVY)
c.alignment = center()
ws7.row_dimensions[1].height = 36
ws7.merge_cells("A2:I2")
c = ws7["A2"]
c.value = "Performed DAILY (or minimum 5x/week) — after main exercise session or as standalone warm-up/cool-down"
c.font = Font(name="Arial", italic=True, size=11, color=C_WHITE)
c.fill = fill(C_TEAL)
c.alignment = center()
ws7.row_dimensions[2].height = 20
ws7.row_dimensions[3].height = 8
# SECTION A: STRETCHING
ws7.merge_cells("A4:I4")
c = ws7.cell(row=4, column=1, value="SECTION A — FLEXIBILITY / STRETCHING")
c.font = Font(name="Arial", bold=True, size=12, color=C_WHITE)
c.fill = fill(C_TEAL)
c.alignment = left()
ws7.row_dimensions[4].height = 24
stretch_headers = ["#", "Stretch", "Target Muscle", "Technique", "Sets", "Duration", "Frequency", "Notes", ""]
ws7.row_dimensions[5].height = 24
for col, hdr in enumerate(stretch_headers, 1):
apply_header(ws7, 5, col, hdr, bg=C_NAVY, sz=11)
stretches = [
(1, "Standing Quad Stretch", "Quadriceps", "Stand on one leg. Bend knee behind, hold ankle. Pull heel toward glute. Keep knees together. Upright posture.", "3", "45-60 sec", "Daily", "Use wall for balance if needed"),
(2, "Prone Quad Stretch", "Quadriceps (deeper)", "Lie face down. Bend knee, hold ankle. Pull gently. Place pillow under thigh if tight. Presses quad into full stretch.", "2", "60 sec", "Daily", "Superior stretch if standing version is limited by balance"),
(3, "Supine Hamstring Stretch", "Hamstrings", "Lie on back. Lift one leg, hold behind thigh. Gently straighten knee. Keep opposite leg flat. Dorsiflex foot for extra stretch.", "3", "45-60 sec per leg", "Daily", "Avoid overpulling — mild stretch only"),
(4, "Seated Forward Bend", "Hamstrings / lumbar", "Sit with legs extended. Hinge forward at hips, reach toward feet. Keep back flat (not rounded). Flex feet.", "2", "45 sec", "Daily", "Hip hinge is key — avoid spinal flexion"),
(5, "Gastrocnemius Stretch\n(Straight-leg)", "Gastrocnemius", "Place hands on wall. Step one foot back, heel flat on floor, knee straight. Lean forward until calf stretch felt.", "3", "45 sec per leg", "Daily", "Tightness here increases patellar tendon load"),
(6, "Soleus Stretch\n(Bent-knee)", "Soleus / deep calf", "Same wall position but with back knee bent. Lean forward. Targets deep soleus — different to gastrocnemius stretch.", "3", "45 sec per leg", "Daily", "Often neglected; critical for ankle dorsiflexion"),
(7, "Hip Flexor Stretch\n(Kneeling Lunge)", "Hip flexors / iliopsoas", "Kneeling lunge position. Back knee down. Push hips forward until anterior hip stretch felt. Upright trunk. Tuck pelvis.", "3", "45 sec per leg", "Daily", "Tight hip flexors alter pelvic tilt and increase patellar tendon loading"),
(8, "Figure-4 Piriformis Stretch", "Piriformis / hip ER", "Lie on back. Cross ankle over opposite knee. Pull both toward chest. Or seated pigeon pose.", "2", "45 sec per side", "Daily", "Hip ER tightness contributes to valgus collapse during landing"),
]
for i, s in enumerate(stretches):
r = 6 + i
ws7.row_dimensions[r].height = 55
bg = C_SKYBLUE if i % 2 == 0 else C_WHITE
for col, val in enumerate(s, 1):
cell = ws7.cell(row=r, column=col, value=val)
cell.font = Font(name="Arial", bold=(col==2), size=10, color=C_DARKTEXT)
cell.fill = fill(bg)
cell.alignment = Alignment(horizontal="center" if col in (1,5,6,7) else "left",
vertical="top", wrap_text=True)
cell.border = thin_border()
# SECTION B: HIP & CORE
sec_b_row = 6 + len(stretches) + 2
ws7.merge_cells(f"A{sec_b_row}:I{sec_b_row}")
c = ws7.cell(row=sec_b_row, column=1, value="SECTION B — HIP & CORE STRENGTHENING")
c.font = Font(name="Arial", bold=True, size=12, color=C_WHITE)
c.fill = fill(C_TEAL)
c.alignment = left()
ws7.row_dimensions[sec_b_row].height = 24
ws7.row_dimensions[sec_b_row+1].height = 24
for col, hdr in enumerate(stretch_headers, 1):
apply_header(ws7, sec_b_row+1, col, hdr, bg=C_NAVY, sz=11)
hip_core_exercises = [
(1, "Clamshells", "Glute medius / hip ER", "Lie on side, hips flexed 45°, knees bent 90°. Feet together. Open top knee like a clamshell (external rotation). Resistance band around knees.", "3", "15-20 reps", "3x/week", "Foundation exercise for hip abductor activation"),
(2, "Side-Lying Hip Abduction", "Glute medius", "Lie on side, lower leg bent for stability. Top leg straight. Lift top leg 30-40°. Control return. Can add ankle weight.", "3", "15 reps per side", "3x/week", "Keep pelvis stacked — avoid rolling back"),
(3, "Lateral Band Walk", "Glute medius / minimus", "Resistance band around ankles. Semi-squat position. Step laterally maintaining tension. 10 steps each direction.", "3", "10 steps each way", "3x/week", "Maintain constant band tension; don't let knees collapse inward"),
(4, "Single-Leg Glute Bridge", "Glute max / hamstrings", "Lie on back, one knee bent, other leg extended. Push through bent heel to lift hips to table-top. Squeeze glutes at top.", "3", "12-15 reps each leg", "3x/week", "Prevents Trendelenburg gait; critical for patellar tracking"),
(5, "Dead Bug", "Deep core stabilizers", "Lie on back, arms up, knees bent 90° in air. Simultaneously lower opposite arm and leg toward floor while keeping lower back flat. Return and repeat.", "3", "10 reps per side", "3x/week", "Lower back must stay pressed to floor throughout — stop if back arches"),
(6, "Plank (Front)", "Core (transverse abdominis, rectus)", "Elbows under shoulders, toes on floor. Body forms straight line. Hold. Brace core, squeeze glutes. No hip sagging.", "3", "30-45 sec\n(build to 60s)", "3x/week", "Progress to plank with leg lift once 45s is stable"),
(7, "Side Plank", "Obliques / glute medius", "Side-lying on elbow. Stack feet. Lift hips to form straight line. Hold. Can progress to side plank with hip abduction.", "3", "20-30 sec per side", "3x/week", "Hip dip indicates weakness — cue hip push to ceiling"),
(8, "Pallof Press", "Anti-rotation core", "Resistance band anchored at chest height. Stand sideways to anchor. Press band straight out, hold 2 sec, return. Anti-rotation challenge.", "3", "10 reps per side", "3x/week", "Functional core stability — directly applicable to sport landing mechanics"),
(9, "Single-Leg Balance / Proprioception", "Ankle, knee, hip stabilizers", "Stand on one leg. Maintain balance for 30-60 sec. Progress: eyes closed, unstable surface (BOSU), add arm movements.", "3", "30-60 sec per leg", "Daily", "Neuromuscular control; critical for return to sport and injury prevention"),
]
for i, ex in enumerate(hip_core_exercises):
r = sec_b_row + 2 + i
ws7.row_dimensions[r].height = 55
bg = "E8F5E9" if i % 2 == 0 else C_WHITE
for col, val in enumerate(ex, 1):
cell = ws7.cell(row=r, column=col, value=val)
cell.font = Font(name="Arial", bold=(col==2), size=10, color=C_DARKTEXT)
cell.fill = fill(bg)
cell.alignment = Alignment(horizontal="center" if col in (1,5,6,7) else "left",
vertical="top", wrap_text=True)
cell.border = thin_border()
set_col_widths(ws7, {"A":5,"B":22,"C":20,"D":36,"E":7,"F":14,"G":12,"H":30,"I":5})
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 8 — 12-WEEK SESSION TRACKER
# ═════════════════════════════════════════════════════════════════════════════
ws8 = wb.create_sheet("12-Week Session Tracker")
ws8.sheet_view.showGridLines = False
ws8.merge_cells("A1:P1")
c = ws8["A1"]
c.value = "12-WEEK SESSION TRACKER — Patellar Tendinopathy Rehabilitation"
c.font = Font(name="Arial", bold=True, size=15, color=C_WHITE)
c.fill = fill(C_NAVY)
c.alignment = center()
ws8.row_dimensions[1].height = 36
ws8.merge_cells("A2:P2")
c = ws8["A2"]
c.value = "Enter data after each session. Yellow cells = data entry. Pain scale: 0 = no pain, 10 = worst imaginable."
c.font = Font(name="Arial", italic=True, size=11, color=C_WHITE)
c.fill = fill(C_TEAL)
c.alignment = center()
ws8.row_dimensions[2].height = 20
ws8.row_dimensions[3].height = 8
# Column headers
tracker_headers = [
"Week","Day","Date","Phase","Session\nType",
"Pre-Session\nPain (NRS 0-10)",
"Pain During\nExercise (NRS)",
"Post-Session\nPain (NRS)",
"24h Post\nPain (NRS)",
"Main Exercise","Sets\nCompleted","Reps\nCompleted",
"Load Used\n(kg / BW)","VISA-P\n(if assessed)","Session RPE\n(0-10)","Notes / Observations"
]
ws8.row_dimensions[4].height = 40
for col, hdr in enumerate(tracker_headers, 1):
apply_header(ws8, 4, col, hdr, bg=C_TEAL, sz=10)
# Phase color map
phase_colors = {1: C_PHASE1, 2: C_PHASE2, 3: C_PHASE3, 4: C_PHASE4}
phase_session_map = {
1: ("Isometric", 1),
2: ("Isometric", 1),
3: ("HSR", 2),
4: ("HSR", 2),
5: ("HSR", 2),
6: ("HSR", 2),
7: ("Eccentric+HSR", 3),
8: ("Eccentric+HSR", 3),
9: ("Eccentric+HSR", 3),
10: ("Eccentric+HSR", 3),
11: ("Return to Sport", 4),
12: ("Return to Sport", 4),
}
# 3 sessions per week for 12 weeks = 36 sessions
row = 5
for week in range(1, 13):
session_type, phase_num = phase_session_map[week]
bg = phase_colors[phase_num]
# Week header row (spans all cols)
ws8.merge_cells(start_row=row, start_column=1, end_row=row, end_column=16)
wc = ws8.cell(row=row, column=1,
value=f"WEEK {week} — Phase {phase_num}: {session_type} "
f"({'Weeks 1-2' if week <= 2 else 'Weeks 3-6' if week <= 6 else 'Weeks 7-10' if week <= 10 else 'Weeks 11-12'})")
wc.font = Font(name="Arial", bold=True, size=11, color=C_WHITE)
wc.fill = fill({1:C_NAVY, 2:C_TEAL, 3:"2D6A4F", 4:C_ORANGE}[phase_num])
wc.alignment = left()
wc.border = thin_border()
ws8.row_dimensions[row].height = 22
row += 1
sessions_per_week = 3 if phase_num >= 2 else 2
day_labels = {2: ["Day 1 (Mon)", "Day 2 (Thu)"],
3: ["Day 1 (Mon)", "Day 2 (Wed)", "Day 3 (Fri)"]}
days = day_labels.get(sessions_per_week, ["Day 1","Day 2","Day 3"])
for day_label in days:
ws8.row_dimensions[row].height = 22
row_bg = bg if (row % 2 == 0) else C_WHITE
values = [week, day_label, "", f"Phase {phase_num}", session_type,
"", "", "", "", "", "", "", "", "", "", ""]
for col, val in enumerate(values, 1):
cell = ws8.cell(row=row, column=col, value=val)
is_entry = col in (3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
cell.font = Font(name="Arial", size=10,
bold=(col in (1,2,4,5)),
color=C_DARKTEXT)
cell.fill = fill(C_YELLOW if is_entry else row_bg)
cell.alignment = Alignment(
horizontal="center" if col not in (10,16) else "left",
vertical="center", wrap_text=True)
cell.border = thin_border()
row += 1
# Weekly summary row
ws8.row_dimensions[row].height = 24
ws8.merge_cells(start_row=row, start_column=1, end_row=row, end_column=5)
sc = ws8.cell(row=row, column=1, value=f"WEEK {week} SUMMARY")
sc.font = Font(name="Arial", bold=True, size=10, color=C_NAVY)
sc.fill = fill(C_LTGRAY)
sc.alignment = left()
sc.border = thin_border()
# Average pain formula (pre-session pain column = col 6)
start_data = row - sessions_per_week
end_data = row - 1
for col, label, formula in [
(6, "Avg Pre-Pain:", f"=IFERROR(AVERAGE(F{start_data}:F{end_data}),\"-\")"),
(8, "Avg Post-Pain:", f"=IFERROR(AVERAGE(H{start_data}:H{end_data}),\"-\")"),
(15, "Avg RPE:", f"=IFERROR(AVERAGE(O{start_data}:O{end_data}),\"-\")"),
]:
# label in col-1, formula in col
lc = ws8.cell(row=row, column=col-1, value=label)
lc.font = Font(name="Arial", bold=True, size=9, color=C_DARKTEXT)
lc.fill = fill(C_LTGRAY)
lc.alignment = right = Alignment(horizontal="right", vertical="center")
lc.border = thin_border()
fc = ws8.cell(row=row, column=col, value=formula)
fc.font = Font(name="Arial", size=10, color=C_TEAL, bold=True)
fc.fill = fill(C_LTGRAY)
fc.alignment = Alignment(horizontal="center", vertical="center")
fc.border = thin_border()
# Fill remaining cols with gray
filled_cols = {1,5,6,7,8,15}
for col in range(6, 17):
if col not in {6, 8, 15, 5, 7, 14}:
cell = ws8.cell(row=row, column=col)
cell.fill = fill(C_LTGRAY)
cell.border = thin_border()
row += 2 # spacer
set_col_widths(ws8, {
"A":7,"B":14,"C":13,"D":10,"E":14,
"F":12,"G":12,"H":12,"I":12,
"J":18,"K":8,"L":8,"M":14,"N":12,"O":10,"P":28
})
# ═════════════════════════════════════════════════════════════════════════════
# SHEET 9 — PROGRESS DASHBOARD
# ═════════════════════════════════════════════════════════════════════════════
ws9 = wb.create_sheet("Progress Dashboard")
ws9.sheet_view.showGridLines = False
ws9.merge_cells("A1:L1")
c = ws9["A1"]
c.value = "PROGRESS DASHBOARD — Patellar Tendinopathy Rehabilitation"
c.font = Font(name="Arial", bold=True, size=15, color=C_WHITE)
c.fill = fill(C_NAVY)
c.alignment = center()
ws9.row_dimensions[1].height = 36
ws9.merge_cells("A2:L2")
c = ws9["A2"]
c.value = "Enter weekly data below. Charts update automatically."
c.font = Font(name="Arial", italic=True, size=11, color=C_WHITE)
c.fill = fill(C_TEAL)
c.alignment = center()
ws9.row_dimensions[2].height = 20
ws9.row_dimensions[3].height = 10
# VISA-P Reference scores
ws9.merge_cells("A4:L4")
c = ws9.cell(row=4, column=1, value="VISA-P SCORING REFERENCE: 0-30 = Severe | 31-50 = Moderate-Severe | 51-70 = Moderate | 71-80 = Mild-Moderate | 81-100 = Minimal / Return to Sport Eligible")
c.font = Font(name="Arial", italic=True, size=10, color=C_DARKTEXT)
c.fill = fill("FFF3CD")
c.alignment = left()
c.border = Border(left=Side(style="medium",color=C_YELLOW),right=Side(style="medium",color=C_YELLOW),
top=Side(style="medium",color=C_YELLOW),bottom=Side(style="medium",color=C_YELLOW))
ws9.row_dimensions[4].height = 22
ws9.row_dimensions[5].height = 8
# Weekly data entry table
ws9.row_dimensions[6].height = 28
dash_headers = ["Week","Phase","Date\n(Assessment)","VISA-P\nScore (0-100)",
"Pre-Session\nPain NRS","Post-Session\nPain NRS","24h Post\nPain NRS",
"Main Load\n(kg)","Reps\n(last set)","Single-Leg\nDecline Squat Pain","Notes",""]
for col, hdr in enumerate(dash_headers, 1):
apply_header(ws9, 6, col, hdr, bg=C_NAVY, sz=11)
phase_labels = {1:"Phase 1", 2:"Phase 1", 3:"Phase 2", 4:"Phase 2",
5:"Phase 2", 6:"Phase 2", 7:"Phase 3", 8:"Phase 3",
9:"Phase 3", 10:"Phase 3", 11:"Phase 4", 12:"Phase 4"}
phase_bg = {1:C_PHASE1,2:C_PHASE1,3:C_PHASE2,4:C_PHASE2,
5:C_PHASE2,6:C_PHASE2,7:C_PHASE3,8:C_PHASE3,
9:C_PHASE3,10:C_PHASE3,11:C_PHASE4,12:C_PHASE4}
for w in range(1, 13):
r = 6 + w
ws9.row_dimensions[r].height = 22
bg = phase_bg[w]
row_vals = [w, phase_labels[w], "", "", "", "", "", "", "", "", "", ""]
for col, val in enumerate(row_vals, 1):
cell = ws9.cell(row=r, column=col, value=val)
is_input = col in (3,4,5,6,7,8,9,10,11)
cell.font = Font(name="Arial", bold=(col in (1,2)), size=11, color=C_DARKTEXT)
cell.fill = fill(C_YELLOW if is_input else bg)
cell.alignment = Alignment(
horizontal="center" if col not in (11,) else "left",
vertical="center")
cell.border = thin_border()
# Add charts
# Line chart: VISA-P score by week
chart_visa = LineChart()
chart_visa.title = "VISA-P Score Progress Over 12 Weeks"
chart_visa.style = 10
chart_visa.y_axis.title = "VISA-P Score (0-100)"
chart_visa.x_axis.title = "Week"
chart_visa.y_axis.scaling.min = 0
chart_visa.y_axis.scaling.max = 100
chart_visa.height = 12
chart_visa.width = 22
data_visa = Reference(ws9, min_col=4, max_col=4, min_row=6, max_row=18)
weeks_ref = Reference(ws9, min_col=1, max_col=1, min_row=7, max_row=18)
chart_visa.add_data(data_visa, titles_from_data=True)
chart_visa.set_categories(weeks_ref)
chart_visa.series[0].graphicalProperties.line.solidFill = C_TEAL
chart_visa.series[0].graphicalProperties.line.width = 25000
chart_visa.series[0].marker.symbol = "circle"
chart_visa.series[0].marker.size = 8
# Add target line at 80
from openpyxl.chart.series import Series
ws9.cell(row=6, column=12, value="Target (80)")
for w in range(1, 13):
ws9.cell(row=6+w, column=12, value=80)
target_ref = Reference(ws9, min_col=12, max_col=12, min_row=6, max_row=18)
chart_visa.add_data(target_ref, titles_from_data=True)
chart_visa.series[1].graphicalProperties.line.solidFill = C_GREEN
chart_visa.series[1].graphicalProperties.line.dashDot = "dash"
chart_visa.series[1].graphicalProperties.line.width = 15000
ws9.add_chart(chart_visa, "A21")
# Bar chart: Pain scores by week
chart_pain = BarChart()
chart_pain.type = "col"
chart_pain.style = 10
chart_pain.title = "Weekly Pain Scores (NRS 0-10)"
chart_pain.y_axis.title = "Pain NRS"
chart_pain.x_axis.title = "Week"
chart_pain.y_axis.scaling.min = 0
chart_pain.y_axis.scaling.max = 10
chart_pain.height = 12
chart_pain.width = 22
data_pre = Reference(ws9, min_col=5, max_col=5, min_row=6, max_row=18)
data_post = Reference(ws9, min_col=7, max_col=7, min_row=6, max_row=18)
chart_pain.add_data(data_pre, titles_from_data=True)
chart_pain.add_data(data_post, titles_from_data=True)
chart_pain.set_categories(weeks_ref)
chart_pain.series[0].graphicalProperties.solidFill = C_ORANGE
chart_pain.series[1].graphicalProperties.solidFill = C_GREEN
ws9.add_chart(chart_pain, "G21")
# Summary stats row
sr = 20
ws9.merge_cells(f"A{sr}:L{sr}")
c = ws9.cell(row=sr, column=1,
value="CHARTS AUTO-UPDATE as you enter weekly data above. Target: VISA-P ≥80 and NRS <2 on SLDS for return to sport clearance.")
c.font = Font(name="Arial", italic=True, size=10, color=C_DARKTEXT)
c.fill = fill("F0F8FF")
c.alignment = left()
ws9.row_dimensions[sr].height = 22
set_col_widths(ws9, {"A":7,"B":10,"C":14,"D":14,"E":13,"F":13,"G":13,
"H":12,"I":10,"J":16,"K":22,"L":14})
# ═════════════════════════════════════════════════════════════════════════════
# Tab colors
# ═════════════════════════════════════════════════════════════════════════════
ws1.sheet_properties.tabColor = C_NAVY
ws2.sheet_properties.tabColor = C_TEAL
for sh_name, col in [
("Rx Phase 1 — Isometric", "A8DADC"),
("Rx Phase 2 — HSR", "52B788"),
("Rx Phase 3 — Eccentric+HSR", "FFD166"),
("Rx Phase 4 — Return to Sport", "E76F51"),
("Stretching & Hip-Core", "84A98C"),
("12-Week Session Tracker", "4A90D9"),
("Progress Dashboard", "9B59B6"),
]:
wb[sh_name].sheet_properties.tabColor = col
# ═════════════════════════════════════════════════════════════════════════════
# SAVE
# ═════════════════════════════════════════════════════════════════════════════
out_path = "/home/daytona/workspace/patellar-rehab/Patellar_Tendinopathy_12Week_Tracker.xlsx"
wb.save(out_path)
print(f"Saved: {out_path}")
cd /home/daytona/workspace/patellar-rehab && python build_tracker.py 2>&1
~/patellar-rehab/build_tracker.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/patellar-rehab/build_tracker.py'
cd /home/daytona/workspace/patellar-rehab && python build_tracker.py 2>&1
~/patellar-rehab/build_tracker.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/patellar-rehab/build_tracker.py'
~/patellar-rehab/build_tracker.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/patellar-rehab/build_tracker.py'
cd /home/daytona/workspace/patellar-rehab && python build_tracker.py 2>&1
python3 -c " import openpyxl wb = openpyxl.Workbook() ws = wb.active ws['A1'] = …
# Check openpyxl version and try with io.BytesIO workaround python3 -c "import o…
~/patellar-rehab/build_tracker.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/patellar-rehab/build_tracker.py'
~/patellar-rehab/build_tracker.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/patellar-rehab/build_tracker.py'
cd /home/daytona/workspace/patellar-rehab && python build_tracker.py 2>&1
ls -lh /home/daytona/workspace/patellar-rehab/Patellar_Tendinopathy_12Week_Track…
Patellar Tendinopathy 12Week Tracker
Spreadsheet · XLSX
| Tab | Color | Contents |
|---|---|---|
| Cover | Navy | Patient info form (yellow input cells), protocol overview, tabs guide |
| Phase Overview | Teal | All 4 phases in one table - weeks, sessions/week, exercises, sets/reps, load, progression rules, pain tolerance |
| Rx Phase 1 - Isometric | Sky blue | Wks 1-2: Wall sit, isometric leg press, isometric step hold - full prescription |
| Rx Phase 2 - HSR | Green | Wks 3-6: Bilateral leg press, hack squat, knee extension, RDL, hip thrust |
| Rx Phase 3 - Eccentric+HSR | Yellow | Wks 7-10: Single-leg decline squat, step-down, Nordic curl, Bulgarian split squat |
| Rx Phase 4 - Return to Sport | Orange | Wks 11-12: CMJ, single-leg hop & stick, depth jump, lateral bound, sprints |
| Stretching & Hip-Core | Sage | 8 stretches + 9 hip/core exercises with sets, duration, frequency, cues |
| 12-Week Session Tracker | Blue | 36 sessions logged - date, pre/during/post/24h NRS, load, reps, weekly averages auto-calculated |
| Progress Dashboard | Purple | Weekly VISA-P + pain NRS data entry table; auto-generates line + bar charts |
Generate image of how to do exercises