diff --git a/frontend/pages/compare/liu-akbar.tsx b/frontend/pages/compare/liu-akbar.tsx new file mode 100644 index 000000000..6cb37a3d3 --- /dev/null +++ b/frontend/pages/compare/liu-akbar.tsx @@ -0,0 +1,489 @@ +import HeaderBar from '@/components/molecules/head' +import { + Table, + TableHeader, + TableColumn, + TableCell, + TableRow, + TableBody, + Chip, +} from '@nextui-org/react' +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js' +import { Line } from 'react-chartjs-2' + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +) + +type Lift = { + date: string + event: string + category: string + bw: number + s1: number + s2: number + s3: number + cj1: number + cj2: number + cj3: number + bestSnatch: number + bestCJ: number + total: number +} + +const LIU_DATA: Lift[] = [ + { date: '2022-12-05', event: '2022 IWF World Championships', category: '89 kg Men', bw: 88.50, s1: 160, s2: 166, s3: -171, cj1: 205, cj2: 211, cj3: 215, bestSnatch: 166, bestCJ: 215, total: 381 }, + { date: '2023-05-05', event: '2023 Asian Championships', category: '96 kg Men', bw: 89.43, s1: -170, s2: 170, s3: 175, cj1: 210, cj2: -223, cj3: -223, bestSnatch: 175, bestCJ: 210, total: 385 }, + { date: '2023-09-04', event: '2023 IWF World Championships', category: '102 kg Men', bw: 98.52, s1: 171, s2: 176, s3: 180, cj1: -215, cj2: 221, cj3: 224, bestSnatch: 180, bestCJ: 224, total: 404 }, + { date: '2023-09-30', event: '19th Asian Games', category: '109 kg Men', bw: 100.80, s1: -175, s2: 180, s3: 185, cj1: 215, cj2: 227, cj3: 233, bestSnatch: 185, bestCJ: 233, total: 418 }, + { date: '2023-12-04', event: '2023 IWF Grand Prix II', category: '102 kg Men', bw: 100.18, s1: 170, s2: -176, s3: 176, cj1: 210, cj2: 222, cj3: -225, bestSnatch: 176, bestCJ: 222, total: 398 }, + { date: '2024-03-31', event: 'IWF World Cup - Paris 2024 Qual. Event', category: '102 kg Men', bw: 101.84, s1: 175, s2: 181, s3: -186, cj1: 220, cj2: 225, cj3: 232, bestSnatch: 181, bestCJ: 232, total: 413 }, + { date: '2024-08-07', event: 'XXXIII Olympic Games', category: '102 kg Men', bw: 101.75, s1: 178, s2: 183, s3: 186, cj1: 220, cj2: -228, cj3: -233, bestSnatch: 186, bestCJ: 220, total: 406 }, + { date: '2025-05-09', event: 'Asian Championships', category: '102 kg Men', bw: 101.49, s1: 171, s2: 180, s3: -183, cj1: 220, cj2: 230, cj3: -234, bestSnatch: 180, bestCJ: 230, total: 410 }, +] + +const DJURAEV_DATA: Lift[] = [ + { date: '2017-06-15', event: '2017 IWF Junior World Championships', category: '105 kg Men', bw: 100.30, s1: 155, s2: 159, s3: 162, cj1: -185, cj2: 185, cj3: 190, bestSnatch: 162, bestCJ: 190, total: 352 }, + { date: '2017-11-27', event: '2017 IWF World Championships', category: '105 kg Men', bw: 103.24, s1: 164, s2: 169, s3: 174, cj1: 194, cj2: 199, cj3: -203, bestSnatch: 174, bestCJ: 199, total: 373 }, + { date: '2018-04-20', event: '2018 Asian Junior Championships', category: '105 kg Men', bw: 102.20, s1: 160, s2: 166, s3: 170, cj1: 191, cj2: 197, cj3: 202, bestSnatch: 170, bestCJ: 202, total: 372 }, + { date: '2018-07-07', event: '2018 IWF Junior World Championships', category: '105 kg Men', bw: 102.35, s1: 167, s2: -172, s3: -172, cj1: 195, cj2: 202, cj3: -210, bestSnatch: 167, bestCJ: 202, total: 369 }, + { date: '2018-11-01', event: '2018 IWF World Championships', category: '102 kg Men', bw: 101.67, s1: 173, s2: 178, s3: 180, cj1: 200, cj2: 207, cj3: 212, bestSnatch: 180, bestCJ: 212, total: 392 }, + { date: '2018-12-19', event: '5th International Qatar Cup', category: '109 kg Men', bw: 104.20, s1: 173, s2: -178, s3: 179, cj1: 200, cj2: 207, cj3: 213, bestSnatch: 179, bestCJ: 213, total: 392 }, + { date: '2019-04-18', event: 'Asian Championships', category: '109 kg Men', bw: 108.01, s1: 176, s2: 181, s3: 185, cj1: 215, cj2: 219, cj3: 225, bestSnatch: 185, bestCJ: 225, total: 410 }, + { date: '2019-06-01', event: '2019 IWF Junior World Championships', category: '109 kg Men', bw: 108.55, s1: 173, s2: 177, s3: 182, cj1: 212, cj2: 216, cj3: 0, bestSnatch: 182, bestCJ: 216, total: 398 }, + { date: '2019-09-18', event: '2019 IWF World Championships', category: '109 kg Men', bw: 108.60, s1: -183, s2: 184, s3: 188, cj1: 221, cj2: 226, cj3: 229, bestSnatch: 188, bestCJ: 229, total: 417 }, + { date: '2019-12-10', event: 'IWF World Cup', category: '109 kg Men', bw: 108.10, s1: 176, s2: 181, s3: -184, cj1: 215, cj2: 219, cj3: -229, bestSnatch: 181, bestCJ: 219, total: 400 }, + { date: '2019-12-19', event: '6th Qatar International Cup', category: '109 kg Men', bw: 108.70, s1: 175, s2: 181, s3: 185, cj1: 215, cj2: 220, cj3: 0, bestSnatch: 185, bestCJ: 220, total: 405 }, + { date: '2020-02-08', event: '6th International Solidarity Championships', category: '109 kg Men', bw: 108.65, s1: 177, s2: 182, s3: 189, cj1: -221, cj2: 221, cj3: 0, bestSnatch: 189, bestCJ: 221, total: 410 }, + { date: '2021-04-17', event: '2020 Asian Championships', category: '109 kg Men', bw: 108.50, s1: 188, s2: 194, s3: -197, cj1: 225, cj2: 234, cj3: -238, bestSnatch: 194, bestCJ: 234, total: 428 }, + { date: '2021-07-23', event: 'XXXII Olympic Games (Tokyo)', category: '109 kg Men', bw: 109.00, s1: -189, s2: 189, s3: 193, cj1: 227, cj2: -234, cj3: 237, bestSnatch: 193, bestCJ: 237, total: 430 }, + { date: '2021-12-07', event: '2021 IWF World Championships', category: '109 kg Men', bw: 108.90, s1: 187, s2: 192, s3: 195, cj1: 226, cj2: 232, cj3: 238, bestSnatch: 195, bestCJ: 238, total: 433 }, + { date: '2022-08-11', event: '5th Islamic Solidarity Games', category: '+109 kg Men', bw: 121.60, s1: -190, s2: 190, s3: 200, cj1: 231, cj2: 242, cj3: 246, bestSnatch: 200, bestCJ: 246, total: 446 }, + { date: '2023-05-05', event: '2023 Asian Championships', category: '+109 kg Men', bw: 126.76, s1: 189, s2: -195, s3: 195, cj1: 230, cj2: -240, cj3: 242, bestSnatch: 195, bestCJ: 242, total: 437 }, + { date: '2023-09-04', event: '2023 IWF World Championships', category: '109 kg Men', bw: 108.84, s1: 182, s2: -189, s3: 189, cj1: 220, cj2: 226, cj3: -231, bestSnatch: 189, bestCJ: 226, total: 415 }, + { date: '2023-09-30', event: '19th Asian Games', category: '109 kg Men', bw: 109.00, s1: 180, s2: 184, s3: 189, cj1: -220, cj2: 222, cj3: 228, bestSnatch: 189, bestCJ: 228, total: 417 }, + { date: '2024-02-03', event: 'Asian Championships', category: '102 kg Men', bw: 101.70, s1: 175, s2: 180, s3: -183, cj1: 214, cj2: -219, cj3: 220, bestSnatch: 180, bestCJ: 220, total: 400 }, + { date: '2024-03-31', event: 'IWF World Cup - Paris 2024 Qual. Event', category: '109 kg Men', bw: 108.50, s1: 180, s2: 185, s3: 189, cj1: 220, cj2: 227, cj3: 0, bestSnatch: 189, bestCJ: 227, total: 416 }, + { date: '2024-08-07', event: 'XXXIII Olympic Games (Paris)', category: '102 kg Men', bw: 102.00, s1: 180, s2: 185, s3: -189, cj1: 219, cj2: -224, cj3: -232, bestSnatch: 185, bestCJ: 219, total: 404 }, + { date: '2025-05-09', event: 'Asian Championships', category: '109 kg Men', bw: 108.91, s1: 180, s2: 183, s3: -189, cj1: 217, cj2: 223, cj3: 0, bestSnatch: 183, bestCJ: 223, total: 406 }, + { date: '2025-10-02', event: '2025 IWF World Championships', category: '110 kg Men', bw: 109.85, s1: 189, s2: 193, s3: 196, cj1: 227, cj2: 232, cj3: -245, bestSnatch: 196, bestCJ: 232, total: 428 }, +] + +type AttemptRate = { made: number; total: number; pct: number } + +function computeAttemptRates(data: Lift[]) { + const slots = [ + { key: 's1' as keyof Lift }, + { key: 's2' as keyof Lift }, + { key: 's3' as keyof Lift }, + { key: 'cj1' as keyof Lift }, + { key: 'cj2' as keyof Lift }, + { key: 'cj3' as keyof Lift }, + ] + return slots.map(({ key }) => { + let made = 0 + let total = 0 + data.forEach(lift => { + const v = lift[key] as number + if (v !== 0) { + total++ + if (v > 0) made++ + } + }) + return { made, total, pct: total > 0 ? Math.round((made / total) * 100) : 0 } as AttemptRate + }) +} + +const HEAD_TO_HEAD = [ + { + date: '2023-09-30', + event: '19th Asian Games', + category: '109 kg Men', + liuTotal: 418, + liuSnatch: 185, + liuCJ: 233, + liuBW: 100.80, + djuraevTotal: 417, + djuraevSnatch: 189, + djuraevCJ: 228, + djuraevBW: 109.00, + winner: 'liu' as const, + margin: 1, + }, + { + date: '2024-08-07', + event: 'XXXIII Olympic Games (Paris)', + category: '102 kg Men', + liuTotal: 406, + liuSnatch: 186, + liuCJ: 220, + liuBW: 101.75, + djuraevTotal: 404, + djuraevSnatch: 185, + djuraevCJ: 219, + djuraevBW: 102.00, + winner: 'liu' as const, + margin: 2, + }, +] + +function formatAttempt(v: number) { + if (v === 0) return 'β€”' + if (v < 0) return {Math.abs(v)} + return {v} +} + +function StatCard({ label, liuVal, djuraevVal }: { label: string; liuVal: string | number; djuraevVal: string | number }) { + const liuNum = typeof liuVal === 'number' ? liuVal : parseFloat(String(liuVal)) + const djNum = typeof djuraevVal === 'number' ? djuraevVal : parseFloat(String(djuraevVal)) + const liuWins = liuNum > djNum + const djWins = djNum > liuNum + + return ( +
+ {label} +
+ {liuVal} + vs + {djuraevVal} +
+
+ ) +} + +function AttemptBar({ rate, color }: { rate: AttemptRate; color: string }) { + return ( +
+
+
+
+ {rate.pct}% +
+ ) +} + +export default function LiuAkbarComparePage() { + const liuRates = computeAttemptRates(LIU_DATA) + const djuraevRates = computeAttemptRates(DJURAEV_DATA) + + const liuBestSnatch = Math.max(...LIU_DATA.map(d => d.bestSnatch)) + const liuBestCJ = Math.max(...LIU_DATA.map(d => d.bestCJ)) + const liuBestTotal = Math.max(...LIU_DATA.map(d => d.total)) + + const djBestSnatch = Math.max(...DJURAEV_DATA.map(d => d.bestSnatch)) + const djBestCJ = Math.max(...DJURAEV_DATA.map(d => d.bestCJ)) + const djBestTotal = Math.max(...DJURAEV_DATA.map(d => d.total)) + + const chartData = { + labels: LIU_DATA.map(d => d.date.slice(0, 7)), + datasets: [ + { + label: 'LIU Huanhua β€” Total', + data: LIU_DATA.map(d => d.total), + borderColor: '#00B0F0', + backgroundColor: '#00B0F020', + tension: 0.3, + pointRadius: 5, + }, + ], + } + + const djChartData = { + labels: DJURAEV_DATA.map(d => d.date.slice(0, 7)), + datasets: [ + { + label: 'DJURAEV Akbar β€” Total', + data: DJURAEV_DATA.map(d => d.total), + borderColor: '#ffce00', + backgroundColor: '#ffce0020', + tension: 0.3, + pointRadius: 5, + }, + ], + } + + const mergedDates = Array.from( + new Set([...LIU_DATA.map(d => d.date), ...DJURAEV_DATA.map(d => d.date)]) + ).sort() + + const liuByDate = Object.fromEntries(LIU_DATA.map(d => [d.date, d.total])) + const djByDate = Object.fromEntries(DJURAEV_DATA.map(d => [d.date, d.total])) + + const overlapDates = mergedDates.filter(d => liuByDate[d] !== undefined && djByDate[d] !== undefined) + + const overlapChartData = { + labels: overlapDates.map(d => d.slice(0, 7)), + datasets: [ + { + label: 'LIU Huanhua', + data: overlapDates.map(d => liuByDate[d]), + borderColor: '#00B0F0', + backgroundColor: '#00B0F020', + tension: 0.3, + pointRadius: 6, + }, + { + label: 'DJURAEV Akbar', + data: overlapDates.map(d => djByDate[d]), + borderColor: '#ffce00', + backgroundColor: '#ffce0020', + tension: 0.3, + pointRadius: 6, + }, + ], + } + + const chartOptions = { + plugins: { legend: { display: true, labels: { color: '#A0A0A0' } } }, + scales: { + x: { grid: { display: false }, ticks: { color: '#A0A0A0', font: { size: 11 } } }, + y: { grid: { display: false }, ticks: { color: '#A0A0A0', font: { size: 11 } } }, + }, + aspectRatio: 2.5, + } + + const attemptLabels = ['1st Snatch', '2nd Snatch', '3rd Snatch', '1st C&J', '2nd C&J', '3rd C&J'] + + return ( + <> + +
+ + {/* Hero */} +
+
+

China πŸ‡¨πŸ‡³

+

LIU Huanhua

+

{LIU_DATA.length} IWF competitions

+
+
VS
+
+

Uzbekistan πŸ‡ΊπŸ‡Ώ

+

DJURAEV Akbar

+

{DJURAEV_DATA.length} IWF competitions

+
+
+ + {/* Career stats */} +
+

Career Bests

+
+ + + + +
+

+ * Djuraev's 446kg total and 200kg snatch were set at the 5th Islamic Solidarity Games 2022 competing at +109kg (121.6kg bodyweight). +

+
+ + {/* Head to head */} +
+

+ Head-to-Head β€” Same Event, Same Weight Class +

+
+ {HEAD_TO_HEAD.map((match, i) => ( +
+
+ {match.event} + Β· + {match.date} + Β· + {match.category} +
+
+
+

+ LIU {match.winner === 'liu' && 'πŸ†'} +

+

BW {match.liuBW} kg

+

{match.liuTotal} kg

+

{match.liuSnatch} / {match.liuCJ}

+
+
+ | + Ξ” {match.margin} kg +
+
+

+ DJURAEV {match.winner === 'djuraev' && 'πŸ†'} +

+

BW {match.djuraevBW} kg

+

{match.djuraevTotal} kg

+

{match.djuraevSnatch} / {match.djuraevCJ}

+
+
+
+ ))} +

+ Liu leads the direct head-to-head record 2–0, winning both encounters by narrow margins (1 kg and 2 kg). +

+
+
+ + {/* Shared-date totals chart */} +
+

+ Total on Same Competition Dates +

+ +
+ + {/* Individual career progression */} +
+

+ Career Total Progression +

+
+
+

LIU Huanhua

+ +
+
+

DJURAEV Akbar

+ +
+
+
+ + {/* Attempt success rates */} +
+

+ Attempt Success Rates +

+
+
+

LIU Huanhua

+ {liuRates.map((rate, i) => ( +
+
+ {attemptLabels[i]} + {rate.made}/{rate.total} +
+ +
+ ))} +
+
+

DJURAEV Akbar

+ {djuraevRates.map((rate, i) => ( +
+
+ {attemptLabels[i]} + {rate.made}/{rate.total} +
+ +
+ ))} +
+
+
+ + {/* Full history tables */} +
+

+ LIU Huanhua β€” Full IWF History +

+
+ + + Date + Event + Cat. + BW + S1 + S2 + S3 + CJ1 + CJ2 + CJ3 + Total + + + {[...LIU_DATA].reverse().map((lift, i) => ( + + {lift.date} + {lift.event} + {lift.category} + {lift.bw} + {formatAttempt(lift.s1)} + {formatAttempt(lift.s2)} + {formatAttempt(lift.s3)} + {formatAttempt(lift.cj1)} + {formatAttempt(lift.cj2)} + {formatAttempt(lift.cj3)} + {lift.total} + + ))} + +
+
+
+ +
+

+ DJURAEV Akbar β€” Full IWF History +

+
+ + + Date + Event + Cat. + BW + S1 + S2 + S3 + CJ1 + CJ2 + CJ3 + Total + + + {[...DJURAEV_DATA].reverse().map((lift, i) => ( + + {lift.date} + {lift.event} + {lift.category} + {lift.bw} + {formatAttempt(lift.s1)} + {formatAttempt(lift.s2)} + {formatAttempt(lift.s3)} + {formatAttempt(lift.cj1)} + {formatAttempt(lift.cj2)} + {formatAttempt(lift.cj3)} + {lift.total} + + ))} + +
+
+
+ + +
+ + ) +} diff --git a/liu_akbar_1_hook.png b/liu_akbar_1_hook.png new file mode 100644 index 000000000..be07eafda Binary files /dev/null and b/liu_akbar_1_hook.png differ diff --git a/liu_akbar_2_charts.png b/liu_akbar_2_charts.png new file mode 100644 index 000000000..49db78a19 Binary files /dev/null and b/liu_akbar_2_charts.png differ diff --git a/liu_akbar_3_stats.png b/liu_akbar_3_stats.png new file mode 100644 index 000000000..90dad4dfe Binary files /dev/null and b/liu_akbar_3_stats.png differ diff --git a/liu_akbar_4_verdict.png b/liu_akbar_4_verdict.png new file mode 100644 index 000000000..15860b9c5 Binary files /dev/null and b/liu_akbar_4_verdict.png differ diff --git a/liu_akbar_5_disciplines.png b/liu_akbar_5_disciplines.png new file mode 100644 index 000000000..26efe7877 Binary files /dev/null and b/liu_akbar_5_disciplines.png differ diff --git a/liu_akbar_6_projection.png b/liu_akbar_6_projection.png new file mode 100644 index 000000000..c8209e9d2 Binary files /dev/null and b/liu_akbar_6_projection.png differ diff --git a/liu_akbar_comparison.png b/liu_akbar_comparison.png new file mode 100644 index 000000000..b833e5f88 Binary files /dev/null and b/liu_akbar_comparison.png differ diff --git a/scripts/compare_liu_akbar.py b/scripts/compare_liu_akbar.py new file mode 100644 index 000000000..0fe87f0df --- /dev/null +++ b/scripts/compare_liu_akbar.py @@ -0,0 +1,899 @@ +""" +3-image Instagram carousel: LIU Huanhua vs DJURAEV Akbar +Image 1 β€” Hook: H2H record + Sinclair equivalence +Image 2 β€” Charts: Total & Sinclair career progressions +Image 3 β€” Stats: Sinclair deep-dive + attempt rates +All outputs: 1080Γ—1080 PNG +""" + +import math, numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +from matplotlib.patches import FancyBboxPatch +from matplotlib.gridspec import GridSpec +from scipy import stats +from datetime import datetime, timedelta +from PIL import Image +from matplotlib.backends.backend_agg import FigureCanvasAgg + +# ── Palette ─────────────────────────────────────────────────────────────────── +BG = '#000000' +CARD = '#0d0d0d' +CARD2 = '#111111' +BORDER = '#222222' +LIU_C = '#00B0F0' +DJ_C = '#ffce00' +WHITE = '#ffffff' +LGRAY = '#A0A0A0' +DGRAY = '#363636' +GREEN = '#598138' +RED = '#9d3d3d' + +# ── Sinclair (exact backend formula) ───────────────────────────────────────── +_COEFFS = { + 2001: (0.938573813, 135.390), 2005: (0.845716976, 168.091), + 2009: (0.784780654, 173.961), 2013: (0.794358141, 174.393), + 2017: (0.75194503, 175.508), 2021: (0.722762521, 193.609), +} +def _coeff(y): + if y <= 2004: return _COEFFS[2001] + elif y <= 2008: return _COEFFS[2005] + elif y <= 2012: return _COEFFS[2009] + elif y <= 2016: return _COEFFS[2013] + elif y <= 2020: return _COEFFS[2017] + else: return _COEFFS[2021] + +def sinc(bw, total, year): + if total <= 0 or bw <= 20: return 0.0 + A, b = _coeff(year) + if bw < b: + X = math.log10(bw / b) + return total * 10**(A * X**2) + return float(total) + +# ── Raw data ────────────────────────────────────────────────────────────────── +LIU_RAW = [ + ('2022-12-05', '2022 IWF WC', '89kg', 88.50, [160, 166,-171], [205, 211, 215], 381), + ('2023-05-05', '2023 Asian Ch.', '96kg', 89.43, [-170,170, 175], [210,-223,-223], 385), + ('2023-09-04', '2023 IWF WC', '102kg', 98.52, [171, 176, 180], [-215,221, 224], 404), + ('2023-09-30', 'Asian Games', '109kg', 100.80, [-175,180, 185], [215, 227, 233], 418), + ('2023-12-04', '2023 IWF GP II', '102kg', 100.18, [170,-176, 176], [210, 222,-225], 398), + ('2024-03-31', 'WC Paris Qual.', '102kg', 101.84, [175, 181,-186], [220, 225, 232], 413), + ('2024-08-07', 'Paris Olympics', '102kg', 101.75, [178, 183, 186], [220,-228,-233], 406), + ('2025-05-09', '2025 Asian Ch.', '102kg', 101.49, [171, 180,-183], [220, 230,-234], 410), +] +DJ_RAW = [ + ('2017-06-15', '2017 Jr WC', '105kg', 100.30, [155, 159, 162], [-185,185, 190], 352), + ('2017-11-27', '2017 IWF WC', '105kg', 103.24, [164, 169, 174], [194, 199,-203], 373), + ('2018-04-20', '2018 Asian Jr.', '105kg', 102.20, [160, 166, 170], [191, 197, 202], 372), + ('2018-07-07', '2018 Jr WC', '105kg', 102.35, [167,-172,-172], [195, 202,-210], 369), + ('2018-11-01', '2018 IWF WC', '102kg', 101.67, [173, 178, 180], [200, 207, 212], 392), + ('2018-12-19', '5th Qatar Cup', '109kg', 104.20, [173,-178, 179], [200, 207, 213], 392), + ('2019-04-18', '2019 Asian Ch.', '109kg', 108.01, [176, 181, 185], [215, 219, 225], 410), + ('2019-06-01', '2019 Jr WC', '109kg', 108.55, [173, 177, 182], [212, 216, 0], 398), + ('2019-09-18', '2019 IWF WC', '109kg', 108.60, [-183,184, 188], [221, 226, 229], 417), + ('2019-12-10', 'IWF World Cup', '109kg', 108.10, [176, 181,-184], [215, 219,-229], 400), + ('2019-12-19', '6th Qatar Cup', '109kg', 108.70, [175, 181, 185], [215, 220, 0], 405), + ('2020-02-08', '6th Solidarity', '109kg', 108.65, [177, 182, 189], [-221,221, 0], 410), + ('2021-04-17', '2020 Asian Ch.', '109kg', 108.50, [188, 194,-197], [225, 234,-238], 428), + ('2021-07-23', 'Tokyo Olympics', '109kg', 109.00, [-189,189, 193], [227,-234, 237], 430), + ('2021-12-07', '2021 IWF WC', '109kg', 108.90, [187, 192, 195], [226, 232, 238], 433), + ('2022-08-11', 'Islamic Sol. G.', '+109kg',121.60, [-190,190, 200], [231, 242, 246], 446), + ('2023-05-05', '2023 Asian Ch.', '+109kg',126.76, [189,-195, 195], [230,-240, 242], 437), + ('2023-09-04', '2023 IWF WC', '109kg', 108.84, [182,-189, 189], [220, 226,-231], 415), + ('2023-09-30', 'Asian Games', '109kg', 109.00, [180, 184, 189], [-220,222, 228], 417), + ('2024-02-03', '2024 Asian Ch.', '102kg', 101.70, [175, 180,-183], [214,-219, 220], 400), + ('2024-03-31', 'WC Paris Qual.', '109kg', 108.50, [180, 185, 189], [220, 227, 0], 416), + ('2024-08-07', 'Paris Olympics', '102kg', 102.00, [180, 185,-189], [219,-224,-232], 404), + ('2025-05-09', '2025 Asian Ch.', '109kg', 108.91, [180, 183,-189], [217, 223, 0], 406), + ('2025-10-02', '2025 IWF WC', '110kg', 109.85, [189, 193, 196], [227, 232,-245], 428), +] + +# ── Derived ─────────────────────────────────────────────────────────────────── +def parse(raw): + dates = [datetime.strptime(r[0],'%Y-%m-%d') for r in raw] + totals = np.array([r[6] for r in raw], dtype=float) + sincs = np.array([sinc(r[3], r[6], int(r[0][:4])) for r in raw]) + return dates, totals, sincs + +def linreg(dates, vals): + x = np.array([(d - dates[0]).days for d in dates], dtype=float) + s, b, r, _, _ = stats.linregress(x, vals) + return s, b, r**2, x + +def attempt_rates(raw): + made = [0]*6; tot = [0]*6 + for r in raw: + for i, v in enumerate(r[4]): + if v != 0: tot[i] += 1; made[i] += int(v > 0) + for i, v in enumerate(r[5]): + if v != 0: tot[i+3] += 1; made[i+3] += int(v > 0) + return [m/t if t else 0 for m, t in zip(made, tot)] + +liu_dates, liu_tot, liu_sin = parse(LIU_RAW) +dj_dates, dj_tot, dj_sin = parse(DJ_RAW) + +liu_ts, liu_tb, liu_tr2, liu_tx = linreg(liu_dates, liu_tot) +dj_ts, dj_tb, dj_tr2, dj_tx = linreg(dj_dates, dj_tot) +liu_ss, liu_sb, liu_sr2, liu_sx = linreg(liu_dates, liu_sin) +dj_ss, dj_sb, dj_sr2, dj_sx = linreg(dj_dates, dj_sin) + +liu_102_sin = np.array([sinc(r[3],r[6],int(r[0][:4])) for r in LIU_RAW if r[2]=='102kg']) +dj_102_sin = np.array([sinc(r[3],r[6],int(r[0][:4])) for r in DJ_RAW if r[2]=='102kg']) +liu_102_tot = np.array([r[6] for r in LIU_RAW if r[2]=='102kg']) +dj_102_tot = np.array([r[6] for r in DJ_RAW if r[2]=='102kg']) + +t_102t, p_102t = stats.ttest_ind(liu_102_tot, dj_102_tot, equal_var=False) +t_102s, p_102s = stats.ttest_ind(liu_102_sin, dj_102_sin, equal_var=False) +pool_s = np.sqrt((np.std(liu_102_sin,ddof=1)**2 + np.std(dj_102_sin,ddof=1)**2)/2) +d_102s = (np.mean(liu_102_sin) - np.mean(dj_102_sin)) / pool_s + +liu_rates = attempt_rates(LIU_RAW) +dj_rates = attempt_rates(DJ_RAW) + +# ── Shared helpers ──────────────────────────────────────────────────────────── +DPI = 150 +TARGET = 1080 +INCH = TARGET / DPI + +def save(fig, path): + canvas = FigureCanvasAgg(fig) + canvas.draw() + buf = canvas.buffer_rgba() + img = Image.frombuffer('RGBA', canvas.get_width_height(), buf, 'raw', 'RGBA', 0, 1) + img = img.convert('RGB') + cw, ch = img.size + if cw != TARGET or ch != TARGET: + pad = Image.new('RGB', (TARGET, TARGET), (0,0,0)) + pad.paste(img, ((TARGET-cw)//2, (TARGET-ch)//2)) + img = pad + img.save(path, dpi=(TARGET, TARGET)) + plt.close(fig) + print(f'Saved {img.size[0]}x{img.size[1]}px {path}') + +def nameplate(ax, n): + """Page indicator n/3 in the corner.""" + ax.text(0.97, 0.97, f'{n} / 3', ha='right', va='top', color=DGRAY, + fontsize=6, transform=ax.transAxes) + +def header(ax, subtitle=''): + ax.set_facecolor(BG); ax.axis('off') + ax.text(0.22, 0.80, 'LIU Huanhua', ha='center', va='center', + color=LIU_C, fontsize=15, fontweight='black', transform=ax.transAxes) + ax.text(0.22, 0.18, 'CHN', ha='center', va='center', + color=LIU_C, fontsize=6.5, alpha=0.65, transform=ax.transAxes) + ax.text(0.50, 0.70, 'vs', ha='center', va='center', + color=DGRAY, fontsize=11, fontweight='bold', transform=ax.transAxes) + ax.text(0.78, 0.80, 'DJURAEV Akbar', ha='center', va='center', + color=DJ_C, fontsize=15, fontweight='black', transform=ax.transAxes) + ax.text(0.78, 0.18, 'UZB', ha='center', va='center', + color=DJ_C, fontsize=6.5, alpha=0.65, transform=ax.transAxes) + if subtitle: + ax.text(0.50, 0.04, subtitle, ha='center', va='center', + color=DGRAY, fontsize=4.5, style='italic', transform=ax.transAxes) + +def card_bg(ax): + ax.set_facecolor(CARD) + for sp in ax.spines.values(): sp.set_edgecolor(BORDER) + ax.axis('off') + +def card_title(ax, title, y=0.95): + ax.text(0.5, y, title, ha='center', va='top', color=WHITE, + fontsize=7, fontweight='bold', transform=ax.transAxes) + +def row3(ax, y, lv, mid, rv, lc=LGRAY, rc=LGRAY, lb=False, rb=False): + ax.text(0.05, y, lv, ha='left', va='center', color=lc, fontsize=6.2, + fontweight='bold' if lb else 'normal', transform=ax.transAxes) + ax.text(0.50, y, mid, ha='center', va='center', color=LGRAY, fontsize=5, + transform=ax.transAxes) + ax.text(0.95, y, rv, ha='right', va='center', color=rc, fontsize=6.2, + fontweight='bold' if rb else 'normal', transform=ax.transAxes) + +def scatter_ax(ax, dates_l, vals_l, dates_d, vals_d, ylabel, + slope_l, int_l, x_l, slope_d, int_d, x_d, + annot_fn=None): + ax.set_facecolor(CARD) + for sp in ax.spines.values(): sp.set_edgecolor(BORDER) + ax.scatter(dates_l, vals_l, color=LIU_C, s=28, zorder=5, alpha=0.90) + ax.scatter(dates_d, vals_d, color=DJ_C, s=28, zorder=5, alpha=0.90) + base_l = dates_l[0]; base_d = dates_d[0] + x_e = np.array([0., x_l[-1] + 60]) + ax.plot([base_l + timedelta(days=float(v)) for v in x_e], + int_l + slope_l * x_e, color=LIU_C, lw=1.4, ls='--', alpha=0.45) + x_e = np.array([0., x_d[-1] + 60]) + ax.plot([base_d + timedelta(days=float(v)) for v in x_e], + int_d + slope_d * x_e, color=DJ_C, lw=1.4, ls='--', alpha=0.45) + ax.axhline(np.mean(vals_l), color=LIU_C, lw=0.6, ls=':', alpha=0.25) + ax.axhline(np.mean(vals_d), color=DJ_C, lw=0.6, ls=':', alpha=0.25) + if annot_fn: annot_fn(ax) + ax.set_ylabel(ylabel, color=LGRAY, fontsize=6) + ax.tick_params(colors=LGRAY, labelsize=5.5, length=2.5) + ax.xaxis.set_tick_params(rotation=30) + +def bars_section(fig, gs_slot, top=0.97): + """Attempt success rates bars. gs_slot is a GridSpec entry spanning full width.""" + ax = fig.add_subplot(gs_slot) + card_bg(ax) + ax.text(0.5, top, 'ATTEMPT SUCCESS RATES', + ha='center', va='top', color=WHITE, + fontsize=7.5, fontweight='bold', transform=ax.transAxes) + + labels = ['S1','S2','S3','CJ1','CJ2','CJ3'] + X_LEFT = 0.01; X_RIGHT = 0.53; BAR_MAX = 0.38 + BAR_H = 0.088; ROW_H = 0.20; ROW_GAP = 0.04 + row_tops = [top-0.14, top-0.14-(ROW_H+ROW_GAP), top-0.14-2*(ROW_H+ROW_GAP)] + + for col in range(2): + x0 = X_LEFT if col == 0 else X_RIGHT + sec = 'SNATCH' if col == 0 else 'C&J' + ax.text(x0 + BAR_MAX*0.5, top-0.04, sec, + ha='center', va='top', color=DGRAY, + fontsize=5.5, fontweight='bold', transform=ax.transAxes) + for row_i in range(3): + idx = col*3 + row_i + lr = liu_rates[idx]; dr = dj_rates[idx] + rt = row_tops[row_i] + y_dj = rt - BAR_H*0.05 + y_liu = rt - BAR_H - BAR_H*0.15 + for y_bar, rate, color in [(y_dj, dr, DJ_C), (y_liu, lr, LIU_C)]: + ax.add_patch(FancyBboxPatch( + (x0, y_bar-BAR_H), BAR_MAX, BAR_H, + boxstyle='round,pad=0.002', facecolor=DGRAY, alpha=0.35, + transform=ax.transAxes, zorder=1)) + if rate > 0: + ax.add_patch(FancyBboxPatch( + (x0, y_bar-BAR_H), BAR_MAX*rate, BAR_H, + boxstyle='round,pad=0.002', facecolor=color, alpha=0.88, + transform=ax.transAxes, zorder=2)) + ax.text(x0-0.005, rt-BAR_H-BAR_H*0.4, labels[idx], + ha='right', va='center', color=WHITE, + fontsize=6.5, fontweight='bold', transform=ax.transAxes) + px = x0 + BAR_MAX + 0.008 + ax.text(px, y_dj -BAR_H*0.5, f'{dr*100:.0f}%', ha='left', va='center', + color=DJ_C, fontsize=5.5, transform=ax.transAxes) + ax.text(px, y_liu-BAR_H*0.5, f'{lr*100:.0f}%', ha='left', va='center', + color=LIU_C, fontsize=5.5, transform=ax.transAxes) + + # legend + for x_leg, color, name in [(0.74, DJ_C, 'DJURAEV'), (0.88, LIU_C, 'LIU')]: + ax.add_patch(FancyBboxPatch((x_leg, 0.93), 0.018, 0.055, + boxstyle='round,pad=0.002', facecolor=color, alpha=0.88, + transform=ax.transAxes)) + ax.text(x_leg+0.022, 0.957, name, ha='left', va='center', + color=color, fontsize=5, transform=ax.transAxes) + return ax + +# ═══════════════════════════════════════════════════════════════════════════════ +# IMAGE 1 β€” HOOK: H2H + Sinclair equivalence +# ═══════════════════════════════════════════════════════════════════════════════ +fig1 = plt.figure(figsize=(INCH, INCH), dpi=DPI, facecolor=BG) +gs1 = GridSpec(4, 2, figure=fig1, + left=0.05, right=0.95, top=0.975, bottom=0.02, + hspace=0.38, wspace=0.26, + height_ratios=[0.09, 0.29, 0.33, 0.29]) + +ax1h = fig1.add_subplot(gs1[0, :]) +header(ax1h, subtitle='Career Head-to-Head Analysis Β· IWF Competitions') +nameplate(ax1h, 1) + +# ── H2H record ── +ax1r = fig1.add_subplot(gs1[1, :]) +card_bg(ax1r) +card_title(ax1r, 'HEAD-TO-HEAD (same event, same weight class)') +ax1r.text(0.50, 0.62, '2 – 0', ha='center', va='center', + color=LIU_C, fontsize=38, fontweight='black', transform=ax1r.transAxes) +ax1r.text(0.50, 0.18, 'LIU leads all direct meetings', + ha='center', va='center', color=LGRAY, fontsize=6.5, transform=ax1r.transAxes) + +# ── Two H2H matchups ── +ax1m1 = fig1.add_subplot(gs1[2, 0]) +ax1m2 = fig1.add_subplot(gs1[2, 1]) + +for ax, ev, cat, dt_str, lt, dt_, ls, ds, lbw, dbw in [ + (ax1m1, 'Asian Games 2023', '109 kg Men', '2023-09-30', + 418, 417, 477.8, 462.5, 100.80, 109.00), + (ax1m2, 'Paris Olympics 2024', '102 kg Men', '2024-08-07', + 406, 404, 462.3, 459.6, 101.75, 102.00), +]: + card_bg(ax) + ax.text(0.5, 0.94, ev, ha='center', va='top', color=WHITE, + fontsize=6.5, fontweight='bold', transform=ax.transAxes) + ax.text(0.5, 0.80, cat, ha='center', va='top', color=LGRAY, + fontsize=5, transform=ax.transAxes) + + # LIU column + ax.text(0.15, 0.60, str(lt), ha='center', va='center', color=LIU_C, + fontsize=20, fontweight='black', transform=ax.transAxes) + ax.text(0.15, 0.40, f'Adj. score\n{ls:.1f}', ha='center', va='center', + color=LIU_C, fontsize=6, alpha=0.8, linespacing=1.5, + transform=ax.transAxes) + ax.text(0.15, 0.22, f'BW {lbw} kg', ha='center', va='center', + color=LGRAY, fontsize=4.8, transform=ax.transAxes) + + # delta + ax.text(0.50, 0.60, f'+{lt-dt_} kg', ha='center', va='center', + color=GREEN, fontsize=7, fontweight='bold', transform=ax.transAxes) + ax.text(0.50, 0.44, 'LIU', ha='center', va='center', + color=LIU_C, fontsize=5.5, fontweight='bold', transform=ax.transAxes) + ax.text(0.50, 0.33, 'wins', ha='center', va='center', + color=LIU_C, fontsize=5, transform=ax.transAxes) + + # DJ column + ax.text(0.85, 0.60, str(dt_), ha='center', va='center', color=LGRAY, + fontsize=20, fontweight='black', transform=ax.transAxes) + ax.text(0.85, 0.40, f'Adj. score\n{ds:.1f}', ha='center', va='center', + color=LGRAY, fontsize=6, alpha=0.8, linespacing=1.5, + transform=ax.transAxes) + ax.text(0.85, 0.22, f'BW {dbw} kg', ha='center', va='center', + color=LGRAY, fontsize=4.8, transform=ax.transAxes) + + ax.text(0.5, 0.06, dt_str, ha='center', va='bottom', color=DGRAY, + fontsize=4.5, transform=ax.transAxes) + +# ── Equivalence banner ── +ax1e = fig1.add_subplot(gs1[3, :]) +card_bg(ax1e) + +# big "=" +ax1e.text(0.50, 0.72, 'THE POUND-FOR-POUND EQUALISER', ha='center', va='center', + color=LGRAY, fontsize=7, fontweight='bold', + transform=ax1e.transAxes) + +ax1e.text(0.18, 0.42, '477.8', ha='center', va='center', color=LIU_C, + fontsize=22, fontweight='black', transform=ax1e.transAxes) +ax1e.text(0.18, 0.22, '418 kg total', ha='center', va='center', + color=LIU_C, fontsize=6, alpha=0.75, transform=ax1e.transAxes) +ax1e.text(0.18, 0.10, '@ 100.8 kg bodyweight', ha='center', va='center', + color=LIU_C, fontsize=5, alpha=0.55, transform=ax1e.transAxes) + +ax1e.text(0.50, 0.38, '=', ha='center', va='center', color=WHITE, + fontsize=24, fontweight='black', transform=ax1e.transAxes) +ax1e.text(0.50, 0.10, 'same pound-for-pound score', ha='center', va='center', + color=LGRAY, fontsize=5, transform=ax1e.transAxes) + +ax1e.text(0.82, 0.42, '477.3', ha='center', va='center', color=DJ_C, + fontsize=22, fontweight='black', transform=ax1e.transAxes) +ax1e.text(0.82, 0.22, '446 kg total', ha='center', va='center', + color=DJ_C, fontsize=6, alpha=0.75, transform=ax1e.transAxes) +ax1e.text(0.82, 0.10, '@ 121.6 kg bodyweight (21 kg heavier)', ha='center', va='center', + color=DJ_C, fontsize=5, alpha=0.55, transform=ax1e.transAxes) + +save(fig1, '/home/user/OpenWeightlifting/liu_akbar_1_hook.png') + +# ═══════════════════════════════════════════════════════════════════════════════ +# IMAGE 2 β€” CHARTS: Total & Sinclair career progressions +# ═══════════════════════════════════════════════════════════════════════════════ +fig2 = plt.figure(figsize=(INCH, INCH), dpi=DPI, facecolor=BG) +gs2 = GridSpec(3, 1, figure=fig2, + left=0.08, right=0.97, top=0.975, bottom=0.02, + hspace=0.38, + height_ratios=[0.07, 0.455, 0.455]) + +ax2h = fig2.add_subplot(gs2[0, :]) +header(ax2h, subtitle='Career Progression Β· IWF Competitions') +nameplate(ax2h, 2) + +# ── Total chart ── +ax2t = fig2.add_subplot(gs2[1]) + +def annot_total(ax): + for hd in [datetime(2023,9,30), datetime(2024,8,7)]: + ax.axvline(hd, color=WHITE, lw=0.7, ls=':', alpha=0.20) + ax.annotate('', xy=(datetime(2023,9,30), 420), xytext=(datetime(2023,9,30), 427), + arrowprops=dict(arrowstyle='->', color=LIU_C, lw=1.0)) + ax.annotate('', xy=(datetime(2023,9,30), 415), xytext=(datetime(2023,9,30), 408), + arrowprops=dict(arrowstyle='->', color=DJ_C, lw=1.0)) + ax.text(datetime(2023,9,30), 429.5, 'Asian Games\n418 v 417', + ha='center', va='bottom', color=WHITE, fontsize=4.8, alpha=0.9, + linespacing=1.5) + ax.annotate('', xy=(datetime(2024,8,7), 407.5), xytext=(datetime(2024,8,7), 414), + arrowprops=dict(arrowstyle='->', color=LIU_C, lw=1.0)) + ax.annotate('', xy=(datetime(2024,8,7), 402), xytext=(datetime(2024,8,7), 395), + arrowprops=dict(arrowstyle='->', color=DJ_C, lw=1.0)) + ax.text(datetime(2024,8,7), 416, 'Paris Olympics\n406 v 404', + ha='center', va='bottom', color=WHITE, fontsize=4.8, alpha=0.9, + linespacing=1.5) + ax.text(datetime(2022,8,11)+timedelta(days=35), 448, + '* Djuraev at +109 kg class (21 kg extra bodyweight)', color=DJ_C, fontsize=4.5, alpha=0.65) + ax.text(0.01, 0.04, + f'Annual improvement Β· Liu: {liu_ts*365:+.1f} kg/yr Djuraev: {dj_ts*365:+.1f} kg/yr', + transform=ax.transAxes, color=LGRAY, fontsize=5.2, va='bottom') + ax.set_title('Raw Total per Competition (kg) β€” Djuraev lifts more on the bar, but is 20+ kg heavier', color=LGRAY, fontsize=7, pad=4) + p1 = mpatches.Patch(color=LIU_C, label='LIU Huanhua') + p2 = mpatches.Patch(color=DJ_C, label='DJURAEV Akbar') + ax.legend(handles=[p1, p2], loc='upper left', facecolor=CARD, + edgecolor=BORDER, labelcolor=WHITE, fontsize=6, + framealpha=0.88, handlelength=1) + +scatter_ax(ax2t, liu_dates, liu_tot, dj_dates, dj_tot, 'Total (kg)', + liu_ts, liu_tb, liu_tx, dj_ts, dj_tb, dj_tx, annot_total) + +# ── Sinclair chart ── +ax2s = fig2.add_subplot(gs2[2]) + +def annot_sinc(ax): + ax.annotate('', xy=(datetime(2023,9,30), 478.5), xytext=(datetime(2023,9,30), 485), + arrowprops=dict(arrowstyle='->', color=LIU_C, lw=1.0)) + ax.text(datetime(2023,9,30), 486.5, '477.8', + ha='center', va='bottom', color=LIU_C, fontsize=5, fontweight='bold') + ax.annotate('', xy=(datetime(2021,12,7), 481), xytext=(datetime(2021,12,7), 487), + arrowprops=dict(arrowstyle='->', color=DJ_C, lw=1.0)) + ax.text(datetime(2021,12,7), 488.5, '480.4', + ha='center', va='bottom', color=DJ_C, fontsize=5, fontweight='bold') + ax.text(datetime(2022,7,1), 422, + 'Djuraev 446 kg at heavier class\n= same adj. score as Liu 418 kg', + ha='center', va='center', color=DJ_C, fontsize=4, alpha=0.70, + linespacing=1.4) + ax.text(0.01, 0.04, + f'Annual improvement Β· Liu: {liu_ss*365:+.1f} pts/yr Djuraev: {dj_ss*365:+.1f} pts/yr', + transform=ax.transAxes, color=LGRAY, fontsize=5.2, va='bottom') + ax.set_title('Adjusted Score (levels the playing field β€” removes bodyweight advantage)', color=LGRAY, fontsize=7, pad=4) + +scatter_ax(ax2s, liu_dates, liu_sin, dj_dates, dj_sin, 'Sinclair', + liu_ss, liu_sb, liu_sx, dj_ss, dj_sb, dj_sx, annot_sinc) + +save(fig2, '/home/user/OpenWeightlifting/liu_akbar_2_charts.png') + +# ═══════════════════════════════════════════════════════════════════════════════ +# IMAGE 3 β€” STATS: Sinclair deep-dive + attempt rates +# ═══════════════════════════════════════════════════════════════════════════════ +fig3 = plt.figure(figsize=(INCH, INCH), dpi=DPI, facecolor=BG) +gs3 = GridSpec(3, 2, figure=fig3, + left=0.05, right=0.97, top=0.975, bottom=0.02, + hspace=0.34, wspace=0.24, + height_ratios=[0.07, 0.40, 0.53]) + +ax3h = fig3.add_subplot(gs3[0, :]) +header(ax3h, subtitle='Statistical Summary Β· IWF Data') +nameplate(ax3h, 3) + +# ── Sinclair stats card ── +a = fig3.add_subplot(gs3[1, 0]) +card_bg(a); card_title(a, 'STRENGTH SCORE (adjusted for bodyweight)') +row3(a, 0.79, f'{max(liu_sin):.1f}', 'Best adj. score', f'{max(dj_sin):.1f}', + lc=LGRAY, rc=DJ_C, rb=True) +row3(a, 0.63, f'{np.mean(liu_sin):.1f}', 'Career avg score', f'{np.mean(dj_sin):.1f}', + lc=LIU_C, rc=LGRAY, lb=True) +row3(a, 0.47, + f'{np.mean(liu_102_sin):.1f}', '102 kg class avg', f'{np.mean(dj_102_sin):.1f}', + lc=LIU_C, rc=LGRAY, lb=True) +row3(a, 0.32, + f'+/-{np.std(liu_102_sin,ddof=1):.1f}', '102 kg variation', f'+/-{np.std(dj_102_sin,ddof=1):.1f}', + lc=LIU_C, rc=LGRAY, lb=True) +row3(a, 0.17, + f'{np.std(liu_102_sin,ddof=1)/np.mean(liu_102_sin)*100:.1f}%', '102 kg consistency', + f'{np.std(dj_102_sin,ddof=1)/np.mean(dj_102_sin)*100:.1f}%', + lc=LIU_C, rc=LGRAY, lb=True) +a.text(0.5, 0.03, + f"Liu's edge at 102 kg is statistically significant (p={p_102s:.2f})", + ha='center', va='bottom', color=DGRAY, fontsize=4.5, transform=a.transAxes) + +# ── Career bests (total vs Sinclair) ── +b = fig3.add_subplot(gs3[1, 1]) +card_bg(b); card_title(b, 'CAREER BESTS (Raw vs Bodyweight-Adjusted)') + +headers = ['', 'Total (kg)', 'Sinclair'] +col_x = [0.08, 0.45, 0.82] +row_ys = [0.79, 0.62, 0.45, 0.28, 0.13] +col_labels = ['', 'Total', 'Sinclair'] + +for xc, lbl in zip(col_x[1:], col_labels[1:]): + b.text(xc, 0.90, lbl, ha='center', va='center', color=LGRAY, + fontsize=5.2, transform=b.transAxes) + +rows = [ + ('Best Snatch', '186 kg', 'β€”'), + ('Best C&J', '233 kg', 'β€”'), + ('Best Total', + f'Liu {max(liu_tot):.0f} / DJ {max(dj_tot):.0f}', + f'Liu {max(liu_sin):.0f} / DJ {max(dj_sin):.0f}'), + ('Career Mean', + f'Liu {np.mean(liu_tot):.0f} / DJ {np.mean(dj_tot):.0f}', + f'Liu {np.mean(liu_sin):.0f} / DJ {np.mean(dj_sin):.0f}'), +] +for (lbl, tv, sv), ry in zip(rows, row_ys): + b.text(0.02, ry, lbl, ha='left', va='center', color=LGRAY, + fontsize=5.2, transform=b.transAxes) + b.text(0.55, ry, tv, ha='center', va='center', color=WHITE, + fontsize=5.2, transform=b.transAxes) + b.text(0.92, ry, sv, ha='right', va='center', + color=LIU_C if 'Liu' in sv and 'DJ' in sv else LGRAY, + fontsize=5.2, transform=b.transAxes) + +b.text(0.5, 0.03, + "Adjusted scores cancel out Djuraev's 28 kg weight advantage", + ha='center', va='bottom', color=LIU_C, fontsize=4.5, + fontweight='bold', transform=b.transAxes) + +# ── Attempt rates (full width, bottom) ── +bars_section(fig3, gs3[2, :], top=0.97) + +save(fig3, '/home/user/OpenWeightlifting/liu_akbar_3_stats.png') + +# ═══════════════════════════════════════════════════════════════════════════════ +# IMAGE 4 β€” VERDICT: Who is the better lifter? +# ═══════════════════════════════════════════════════════════════════════════════ +liu_cv102 = np.std(liu_102_sin, ddof=1) / np.mean(liu_102_sin) * 100 +dj_cv102 = np.std(dj_102_sin, ddof=1) / np.mean(dj_102_sin) * 100 +liu_overall_rate = np.mean(liu_rates) * 100 +dj_overall_rate = np.mean(dj_rates) * 100 + +scorecard = [ + ('Head-to-Head', '2 – 0', '0 – 2', 'liu'), + ('Career Best Total', f'{max(liu_tot):.0f} kg', f'{max(dj_tot):.0f} kg', 'dj'), + ('Best adj. score', f'{max(liu_sin):.1f}', f'{max(dj_sin):.1f}', 'dj'), + ('Avg adj. score', f'{np.mean(liu_sin):.1f}', f'{np.mean(dj_sin):.1f}', 'liu'), + ('Same class score (102 kg)', f'{np.mean(liu_102_sin):.1f}', f'{np.mean(dj_102_sin):.1f}', 'liu'), + ('Consistency', f'{liu_cv102:.1f}%', f'{dj_cv102:.1f}%', 'liu'), + ('Improvement rate', f'{liu_ss*365:+.1f}', f'{dj_ss*365:+.1f}', 'dj'), + ('Attempt Rate', f'{liu_overall_rate:.0f}%', f'{dj_overall_rate:.0f}%', + 'liu' if liu_overall_rate >= dj_overall_rate else 'dj'), +] + +liu_score = sum(1 for *_, w in scorecard if w == 'liu') +dj_score = sum(1 for *_, w in scorecard if w == 'dj') + +fig4 = plt.figure(figsize=(INCH, INCH), dpi=DPI, facecolor=BG) +gs4 = GridSpec(3, 1, figure=fig4, + left=0.05, right=0.97, top=0.975, bottom=0.02, + hspace=0.22, + height_ratios=[0.08, 0.55, 0.37]) + +ax4h = fig4.add_subplot(gs4[0]) +header(ax4h, subtitle='Data-driven verdict Β· Who is the better lifter?') +ax4h.text(0.97, 0.97, '4 / 4', ha='right', va='top', color=DGRAY, + fontsize=6, transform=ax4h.transAxes) + +# ── Scorecard ── +ax4s = fig4.add_subplot(gs4[1]) +card_bg(ax4s) +ax4s.text(0.5, 0.97, 'METRIC SCORECARD', ha='center', va='top', color=WHITE, + fontsize=7.5, fontweight='bold', transform=ax4s.transAxes) + +ax4s.text(0.38, 0.89, str(liu_score), ha='center', va='center', color=LIU_C, + fontsize=20, fontweight='black', transform=ax4s.transAxes) +ax4s.text(0.50, 0.89, '–', ha='center', va='center', color=LGRAY, + fontsize=14, transform=ax4s.transAxes) +ax4s.text(0.62, 0.89, str(dj_score), ha='center', va='center', color=DJ_C, + fontsize=20, fontweight='black', transform=ax4s.transAxes) +ax4s.text(0.38, 0.79, 'LIU', ha='center', va='center', color=LIU_C, + fontsize=5, alpha=0.7, transform=ax4s.transAxes) +ax4s.text(0.62, 0.79, 'DJURAEV', ha='center', va='center', color=DJ_C, + fontsize=5, alpha=0.7, transform=ax4s.transAxes) + +ax4s.plot([0.03, 0.97], [0.745, 0.745], color=BORDER, lw=0.8, + transform=ax4s.transAxes) + +n_rows = len(scorecard) +y0 = 0.700 +dy = y0 / (n_rows + 0.5) + +for i, (label, lv, dv, winner) in enumerate(scorecard): + y = y0 - i * dy - dy * 0.4 + + ax4s.add_patch(FancyBboxPatch( + (0.03, y - dy * 0.44), 0.94, dy * 0.86, + boxstyle='round,pad=0.003', + facecolor=LIU_C if winner == 'liu' else DJ_C, + alpha=0.07, transform=ax4s.transAxes, zorder=1)) + + lc = LIU_C if winner == 'liu' else DGRAY + dc = DJ_C if winner == 'dj' else DGRAY + lw = 'bold' if winner == 'liu' else 'normal' + dw = 'bold' if winner == 'dj' else 'normal' + + ax4s.text(0.31, y, lv, ha='right', va='center', color=lc, + fontsize=6.5, fontweight=lw, transform=ax4s.transAxes, zorder=2) + ax4s.text(0.50, y, label, ha='center', va='center', color=LGRAY, + fontsize=5.0, transform=ax4s.transAxes, zorder=2) + ax4s.text(0.69, y, dv, ha='left', va='center', color=dc, + fontsize=6.5, fontweight=dw, transform=ax4s.transAxes, zorder=2) + + dot_x = 0.09 if winner == 'liu' else 0.91 + ax4s.plot(dot_x, y, 'o', color=LIU_C if winner == 'liu' else DJ_C, + ms=3.5, transform=ax4s.transAxes, zorder=3, clip_on=False) + +# ── Verdict ── +ax4v = fig4.add_subplot(gs4[2]) +card_bg(ax4v) +ax4v.text(0.50, 0.96, 'THE VERDICT', ha='center', va='top', color=LGRAY, + fontsize=7, fontweight='bold', transform=ax4v.transAxes) +ax4v.plot([0.05, 0.95], [0.87, 0.87], color=BORDER, lw=0.6, + transform=ax4v.transAxes) +ax4v.text(0.50, 0.76, 'LIU Huanhua', ha='center', va='center', color=LIU_C, + fontsize=22, fontweight='black', transform=ax4v.transAxes) +ax4v.text(0.50, 0.58, 'POUND-FOR-POUND CHAMPION', ha='center', va='center', + color=LIU_C, fontsize=8, fontweight='bold', alpha=0.80, + transform=ax4v.transAxes) +ax4v.plot([0.05, 0.95], [0.50, 0.50], color=BORDER, lw=0.6, + transform=ax4v.transAxes) + +for ri, txt in enumerate([ + '● 2–0 head-to-head in direct competition', + '● Statistically dominant when both compete at 102 kg', + '● Far more consistent β€” half the performance variation', +]): + ax4v.text(0.50, 0.41 - ri * 0.13, txt, ha='center', va='center', + color=LGRAY, fontsize=5.5, transform=ax4v.transAxes) + +ax4v.text(0.50, 0.04, + f"Djuraev's bigger total (+{int(max(dj_tot) - max(liu_tot))} kg) " + 'reflects bodyweight advantage Β· OpenWeightlifting', + ha='center', va='bottom', color=DGRAY, fontsize=4.2, + transform=ax4v.transAxes) + +save(fig4, '/home/user/OpenWeightlifting/liu_akbar_4_verdict.png') + +# ═══════════════════════════════════════════════════════════════════════════════ +# IMAGE 5 β€” DISCIPLINES: Snatch & Clean and Jerk career progressions +# ═══════════════════════════════════════════════════════════════════════════════ +def best_s(r): return max((a for a in r[4] if a > 0), default=0) +def best_c(r): return max((a for a in r[5] if a > 0), default=0) + +liu_sn = np.array([best_s(r) for r in LIU_RAW], dtype=float) +liu_cj = np.array([best_c(r) for r in LIU_RAW], dtype=float) +dj_sn = np.array([best_s(r) for r in DJ_RAW], dtype=float) +dj_cj = np.array([best_c(r) for r in DJ_RAW], dtype=float) + +liu_sns, liu_snb, liu_snr2, liu_snx = linreg(liu_dates, liu_sn) +dj_sns, dj_snb, dj_snr2, dj_snx = linreg(dj_dates, dj_sn) +liu_cjs, liu_cjb, liu_cjr2, liu_cjx = linreg(liu_dates, liu_cj) +dj_cjs, dj_cjb, dj_cjr2, dj_cjx = linreg(dj_dates, dj_cj) + +fig5 = plt.figure(figsize=(INCH, INCH), dpi=DPI, facecolor=BG) +gs5 = GridSpec(3, 1, figure=fig5, + left=0.08, right=0.97, top=0.975, bottom=0.02, + hspace=0.40, + height_ratios=[0.07, 0.455, 0.455]) + +ax5h = fig5.add_subplot(gs5[0]) +header(ax5h, subtitle='Snatch & Clean and Jerk Β· Career Progression') +ax5h.text(0.97, 0.97, '5 / 5', ha='right', va='top', color=DGRAY, + fontsize=6, transform=ax5h.transAxes) + +# ── Snatch chart ── +ax5s = fig5.add_subplot(gs5[1]) + +def annot_snatch(ax): + li = int(np.argmax(liu_sn)) + di = int(np.argmax(dj_sn)) + for idx, dates, vals, color, note in [ + (li, liu_dates, liu_sn, LIU_C, ''), + (di, dj_dates, dj_sn, DJ_C, '* competed at heavier class'), + ]: + ax.annotate('', xy=(dates[idx], vals[idx]), + xytext=(dates[idx], vals[idx] + 4), + arrowprops=dict(arrowstyle='->', color=color, lw=1.0)) + ax.text(dates[idx], vals[idx] + 5.5, + f'{int(vals[idx])} kg', + ha='center', va='bottom', color=color, fontsize=5, fontweight='bold') + if note: + ax.text(dates[idx], vals[idx] - 8, note, + ha='center', color=color, fontsize=4, alpha=0.65) + ax.set_title('Best Snatch per Competition (Djuraev lifts more β€” but outweighs Liu by 20+ kg)', color=LGRAY, fontsize=7, pad=4) + p1 = mpatches.Patch(color=LIU_C, + label=f'LIU best {int(max(liu_sn))} kg | mean {np.mean(liu_sn):.0f} kg') + p2 = mpatches.Patch(color=DJ_C, + label=f'DJURAEV best {int(max(dj_sn))} kg | mean {np.mean(dj_sn):.0f} kg') + ax.legend(handles=[p1, p2], loc='upper left', facecolor=CARD, + edgecolor=BORDER, labelcolor=WHITE, fontsize=5.5, + framealpha=0.88, handlelength=1) + ax.text(0.01, 0.04, + f'Annual improvement Β· Liu: {liu_sns*365:+.1f} kg/yr Djuraev: {dj_sns*365:+.1f} kg/yr', + transform=ax.transAxes, color=LGRAY, fontsize=5.2, va='bottom') + +scatter_ax(ax5s, liu_dates, liu_sn, dj_dates, dj_sn, 'Snatch (kg)', + liu_sns, liu_snb, liu_snx, dj_sns, dj_snb, dj_snx, annot_snatch) + +# ── C&J chart ── +ax5c = fig5.add_subplot(gs5[2]) + +def annot_cj(ax): + li = int(np.argmax(liu_cj)) + di = int(np.argmax(dj_cj)) + for idx, dates, vals, color, note in [ + (li, liu_dates, liu_cj, LIU_C, ''), + (di, dj_dates, dj_cj, DJ_C, '* competed at heavier class'), + ]: + ax.annotate('', xy=(dates[idx], vals[idx]), + xytext=(dates[idx], vals[idx] + 4), + arrowprops=dict(arrowstyle='->', color=color, lw=1.0)) + ax.text(dates[idx], vals[idx] + 5.5, + f'{int(vals[idx])} kg', + ha='center', va='bottom', color=color, fontsize=5, fontweight='bold') + if note: + ax.text(dates[idx], vals[idx] - 9, note, + ha='center', color=color, fontsize=4, alpha=0.65) + ax.set_title('Best Clean & Jerk per Competition (career averages nearly identical despite bodyweight gap)', color=LGRAY, fontsize=7, pad=4) + p1 = mpatches.Patch(color=LIU_C, + label=f'LIU best {int(max(liu_cj))} kg | mean {np.mean(liu_cj):.0f} kg') + p2 = mpatches.Patch(color=DJ_C, + label=f'DJURAEV best {int(max(dj_cj))} kg | mean {np.mean(dj_cj):.0f} kg') + ax.legend(handles=[p1, p2], loc='upper left', facecolor=CARD, + edgecolor=BORDER, labelcolor=WHITE, fontsize=5.5, + framealpha=0.88, handlelength=1) + ax.text(0.01, 0.04, + f'Annual improvement Β· Liu: {liu_cjs*365:+.1f} kg/yr Djuraev: {dj_cjs*365:+.1f} kg/yr', + transform=ax.transAxes, color=LGRAY, fontsize=5.2, va='bottom') + +scatter_ax(ax5c, liu_dates, liu_cj, dj_dates, dj_cj, 'C&J (kg)', + liu_cjs, liu_cjb, liu_cjx, dj_cjs, dj_cjb, dj_cjx, annot_cj) + +save(fig5, '/home/user/OpenWeightlifting/liu_akbar_5_disciplines.png') + +# ═══════════════════════════════════════════════════════════════════════════════ +# IMAGE 6 β€” PROJECTION: Who wins in September 2026? +# ═══════════════════════════════════════════════════════════════════════════════ +target = datetime(2026, 9, 30) + +def project(dates, vals, slope, intercept, x_hist, target_dt, ci=0.90): + """Point estimate + symmetric prediction interval at target_dt.""" + x_pred = float((target_dt - dates[0]).days) + n = len(x_hist) + y_fit = intercept + slope * x_hist + s_err = np.sqrt(np.sum((vals - y_fit) ** 2) / (n - 2)) + x_mean = np.mean(x_hist) + se = s_err * np.sqrt(1 + 1/n + (x_pred - x_mean)**2 + / np.sum((x_hist - x_mean)**2)) + t_val = stats.t.ppf((1 + ci) / 2, df=n - 2) + y_pred = intercept + slope * x_pred + return y_pred, y_pred - t_val * se, y_pred + t_val * se + +def proj_band(vals, slope, intercept, x_hist, x0, x1, ci=0.90, n_pts=60): + """Arrays for plotting the expanding prediction cone.""" + xs = np.linspace(x0, x1, n_pts) + n = len(x_hist) + y_fit = intercept + slope * x_hist + s_err = np.sqrt(np.sum((vals - y_fit) ** 2) / (n - 2)) + x_mean = np.mean(x_hist) + se = s_err * np.sqrt(1 + 1/n + (xs - x_mean)**2 + / np.sum((x_hist - x_mean)**2)) + t_val = stats.t.ppf((1 + ci) / 2, df=n - 2) + y_mid = intercept + slope * xs + return xs, y_mid, y_mid - t_val * se, y_mid + t_val * se + +liu_pt, liu_lo, liu_hi = project(liu_dates, liu_tot, liu_ts, liu_tb, liu_tx, target) +dj_pt, dj_lo, dj_hi = project(dj_dates, dj_tot, dj_ts, dj_tb, dj_tx, target) +liu_spt, liu_slo, liu_shi = project(liu_dates, liu_sin, liu_ss, liu_sb, liu_sx, target) +dj_spt, dj_slo, dj_shi = project(dj_dates, dj_sin, dj_ss, dj_sb, dj_sx, target) +liu_snpt, *_ = project(liu_dates, liu_sn, liu_sns, liu_snb, liu_snx, target) +liu_cjpt, *_ = project(liu_dates, liu_cj, liu_cjs, liu_cjb, liu_cjx, target) +dj_snpt, *_ = project(dj_dates, dj_sn, dj_sns, dj_snb, dj_snx, target) +dj_cjpt, *_ = project(dj_dates, dj_cj, dj_cjs, dj_cjb, dj_cjx, target) + +# Both compete at 110 kg β€” Liu's total projected via Sinclair conversion. +# We project Liu's future Sinclair, then back-calculate what that equates to +# at 110 kg BW using the same IWF formula. Djuraev already trains at ~110 kg +# so his raw-total regression is used directly. +def sinc_factor(bw, year): + A, b = _coeff(year) + if bw < b: + X = math.log10(bw / b) + return 10 ** (A * X ** 2) + return 1.0 + +sf110 = sinc_factor(110.0, 2026) +liu_pt110 = liu_spt / sf110 # projected total at 110 kg BW +liu_lo110 = liu_slo / sf110 +liu_hi110 = liu_shi / sf110 +# Scale snatch / C&J breakdown by the same ratio +_scale = liu_pt110 / liu_pt +liu_snpt110 = liu_snpt * _scale +liu_cjpt110 = liu_cjpt * _scale + +fig6 = plt.figure(figsize=(INCH, INCH), dpi=DPI, facecolor=BG) +gs6 = GridSpec(3, 2, figure=fig6, + left=0.08, right=0.95, top=0.975, bottom=0.02, + hspace=0.32, wspace=0.22, + height_ratios=[0.07, 0.50, 0.43]) + +ax6h = fig6.add_subplot(gs6[0, :]) +header(ax6h, subtitle='Who wins the next major? Β· September 2026 projection') +ax6h.text(0.97, 0.97, '6 / 6', ha='right', va='top', color=DGRAY, + fontsize=6, transform=ax6h.transAxes) + +# ── Projection chart ── +ax6c = fig6.add_subplot(gs6[1, :]) +ax6c.set_facecolor(CARD) +for sp in ax6c.spines.values(): sp.set_edgecolor(BORDER) + +ax6c.scatter(liu_dates, liu_tot, color=LIU_C, s=22, zorder=5, alpha=0.85) +ax6c.scatter(dj_dates, dj_tot, color=DJ_C, s=22, zorder=5, alpha=0.85) + +for dates, x_h, slope, intercept, vals, color in [ + (liu_dates, liu_tx, liu_ts, liu_tb, liu_tot, LIU_C), + (dj_dates, dj_tx, dj_ts, dj_tb, dj_tot, DJ_C), +]: + base = dates[0] + # Historical trend (dashed) + x_e = np.array([0., float(x_h[-1])]) + ax6c.plot([base + timedelta(days=v) for v in x_e], + intercept + slope * x_e, color=color, lw=1.1, ls='--', alpha=0.35) + # Projection cone + xs, ys, lo_arr, hi_arr = proj_band( + vals, slope, intercept, x_h, + float(x_h[-1]), (target - base).days) + ts_ = [base + timedelta(days=float(v)) for v in xs] + ax6c.plot(ts_, ys, color=color, lw=1.3, ls=':', alpha=0.72) + ax6c.fill_between(ts_, lo_arr, hi_arr, color=color, alpha=0.08) + # Clean diamond marker at projected point β€” no cluttering text labels + ax6c.plot(target, intercept + slope * (target - base).days, + 'D', color=color, ms=7, zorder=8, + markeredgecolor=BG, markeredgewidth=0.5) + +ax6c.axvline(target, color=WHITE, lw=0.7, ls=':', alpha=0.25) +ax6c.text(target, 0.01, 'Sep 2026', ha='center', va='bottom', + color=LGRAY, fontsize=5, transform=ax6c.get_xaxis_transform()) + +ax6c.set_xlim(right=target + timedelta(days=40)) +ax6c.set_ylabel('Total (kg)', color=LGRAY, fontsize=6) +ax6c.tick_params(colors=LGRAY, labelsize=5.5, length=2.5) +ax6c.xaxis.set_tick_params(rotation=30) +ax6c.set_title( + 'Career total with Sep 2026 projection Β· shaded area = likely range Β· β—† = projected result', + color=LGRAY, fontsize=5.5, pad=4) + +p1 = mpatches.Patch(color=LIU_C, + label=f'LIU β—† {liu_pt:.0f} kg projected (expected: {liu_lo:.0f}–{liu_hi:.0f})') +p2 = mpatches.Patch(color=DJ_C, + label=f'DJURAEV β—† {dj_pt:.0f} kg projected (expected: {dj_lo:.0f}–{dj_hi:.0f})') +ax6c.legend(handles=[p1, p2], loc='upper left', facecolor=CARD, + edgecolor=BORDER, labelcolor=WHITE, fontsize=5.5, + framealpha=0.88, handlelength=1) + +# ── Prediction cards ── +ax6l = fig6.add_subplot(gs6[2, 0]) +ax6r = fig6.add_subplot(gs6[2, 1]) + +winner110 = 'LIU' if liu_pt110 > dj_pt else 'DJURAEV' +win_color = LIU_C if winner110 == 'LIU' else DJ_C + +for ax, name, color, pt, lo, hi, snpt, cjpt, wt, note in [ + (ax6l, 'LIU Huanhua', LIU_C, + liu_pt110, liu_lo110, liu_hi110, liu_snpt110, liu_cjpt110, + '110 kg class', 'Projected after moving up to 110 kg'), + (ax6r, 'DJURAEV Akbar', DJ_C, + dj_pt, dj_lo, dj_hi, dj_snpt, dj_cjpt, + '110 kg class (current class)', ''), +]: + card_bg(ax) + ax.text(0.50, 0.97, name, ha='center', va='top', color=color, + fontsize=8.5, fontweight='black', transform=ax.transAxes) + ax.text(0.50, 0.83, wt, ha='center', va='center', color=LGRAY, + fontsize=4.8, transform=ax.transAxes) + if note: + ax.text(0.50, 0.73, note, ha='center', va='center', color=DGRAY, + fontsize=4.0, style='italic', transform=ax.transAxes) + ax.text(0.50, 0.58, f'{pt:.0f} kg', ha='center', va='center', color=color, + fontsize=17, fontweight='black', transform=ax.transAxes) + ax.text(0.50, 0.43, f'Expected range: {lo:.0f} – {hi:.0f} kg', + ha='center', va='center', color=color, fontsize=4.8, alpha=0.60, + transform=ax.transAxes) + ax.text(0.25, 0.28, f'~{snpt:.0f}', ha='center', va='center', + color=LGRAY, fontsize=7, fontweight='bold', transform=ax.transAxes) + ax.text(0.25, 0.16, 'Snatch', ha='center', va='center', + color=DGRAY, fontsize=4.5, transform=ax.transAxes) + ax.text(0.50, 0.28, '+', ha='center', va='center', + color=DGRAY, fontsize=6, transform=ax.transAxes) + ax.text(0.75, 0.28, f'~{cjpt:.0f}', ha='center', va='center', + color=LGRAY, fontsize=7, fontweight='bold', transform=ax.transAxes) + ax.text(0.75, 0.16, 'C&J', ha='center', va='center', + color=DGRAY, fontsize=4.5, transform=ax.transAxes) + margin = abs(liu_pt110 - dj_pt) + ax.text(0.50, 0.05, + f'Same class β€” direct comparison Β· ' + f'projected margin: {margin:.0f} kg', + ha='center', va='bottom', color=DGRAY, fontsize=3.8, + transform=ax.transAxes) + +# Verdict strip along bottom +fig6.text(0.5, 0.022, + f'PROJECTED WINNER AT 110 KG: {winner110} ' + f'({liu_pt110:.0f} vs {dj_pt:.0f} kg) Β· ' + f'Expected ranges overlap β€” either could win Β· OpenWeightlifting', + ha='center', va='bottom', color=win_color, + fontsize=5.5, fontweight='bold') + +save(fig6, '/home/user/OpenWeightlifting/liu_akbar_6_projection.png')