Kaliman-1981 commited on
Commit
0f7fe98
·
verified ·
1 Parent(s): 951cfc4

Add 3 files

Browse files
Files changed (3) hide show
  1. README.md +6 -4
  2. index.html +818 -18
  3. prompts.txt +0 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Prdtrend1 0
3
- emoji: 📊
4
- colorFrom: indigo
5
  colorTo: blue
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: prdtrend1-0
3
+ emoji: ⚛️
4
+ colorFrom: green
5
  colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - QwenSite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,819 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Curve Master Productivity Monitor</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ :root{
9
+ --bg:#0f172a; --panel:#101827; --panel2:#0b1222; --text:#e5e7eb; --muted:#94a3b8;
10
+ --teal:#06b6d4; --sky:#38bdf8; --blue:#2563eb; --green:#22c55e; --amber:#facc15; --red:#ef4444; --border:#20304d;
11
+ --radius:14px; --gap:16px;
12
+ }
13
+ *{box-sizing:border-box}
14
+ body{margin:0;font-family:Arial,Helvetica,sans-serif;background:var(--bg);color:var(--text)}
15
+
16
+ /* ===== Brand header ===== */
17
+ header{
18
+ display:flex;align-items:center;justify-content:space-between;gap:12px;
19
+ background:#0c1526; padding:12px 20px;border-bottom:1px solid rgba(255,255,255,.08)
20
+ }
21
+ .brand{display:flex;align-items:center;gap:12px}
22
+ .brand-icon{
23
+ width:36px;height:36px;border-radius:10px;background:rgba(255,255,255,.06);
24
+ display:grid;place-items:center;border:1px solid rgba(255,255,255,.18);
25
+ box-shadow:inset 0 0 12px rgba(255,255,255,.06);
26
+ }
27
+ .brand-icon svg{width:22px;height:22px}
28
+ .brand-text{line-height:1}
29
+ .brand-title{
30
+ font-family:Georgia,"Times New Roman",serif;
31
+ font-size:28px; letter-spacing:.2px; color:#dbeafe;
32
+ text-shadow:0 1px 0 rgba(0,0,0,.5);
33
+ }
34
+ .brand-sub{margin-top:2px; font-size:12px; color:#b9c8ff; opacity:.95}
35
+ .hdr-actions{display:flex;gap:8px}
36
+ .btn{height:34px;padding:6px 12px;border-radius:10px;border:1px solid rgba(255,255,255,.25);background:rgba(255,255,255,.08);color:#fff;cursor:pointer;font-weight:600}
37
+ .btn:hover{filter:brightness(1.06)}
38
+ .btn-danger{background:#dc2626;border-color:#b91c1c}
39
+
40
+ main{max-width:1200px;margin:20px auto;padding:0 var(--gap)}
41
+ .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--gap)}
42
+ .card{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:16px}
43
+ .span-4{grid-column:span 4}.span-8{grid-column:span 8}.span-12{grid-column:span 12}
44
+ h2{margin:0 0 12px 0;font-size:18px;color:#e6f9ff}
45
+ .sub{font-size:12px;color:var(--muted);margin-bottom:10px}
46
+
47
+ .uploader{border:2px dashed var(--sky);padding:16px;text-align:center;border-radius:var(--radius);background:var(--panel2)}
48
+ input[type="file"]{display:block;margin:8px auto;color:var(--muted)}
49
+ label{font-size:12px;color:var(--muted)}
50
+ .controls-row{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
51
+ input[type="text"], input[type="number"]{
52
+ background:#0b162c;color:var(--text);border:1px solid var(--border);
53
+ padding:8px 10px;border-radius:10px;outline:none;height:36px
54
+ }
55
+ .num{width:110px}
56
+ .tog{display:flex;gap:8px;align-items:center;font-size:12px;color:var(--muted);height:36px}
57
+ .pill{padding:6px 12px;border-radius:999px;background:#0b162c;border:1px solid var(--border);color:#cbd5e1;font-size:12px;cursor:pointer}
58
+
59
+ /* Contractor picker */
60
+ #contractorBox{width:100%;height:260px;border:1px solid var(--border);border-radius:10px;background:#0b162c;overflow:auto;padding:6px}
61
+ .c-item{display:flex;align-items:center;gap:8px;padding:6px 6px;border-radius:8px}
62
+ .c-item:hover{background:#0c1426}
63
+
64
+ /* Table */
65
+ table{width:100%;border-collapse:collapse;font-size:13px}
66
+ th,td{padding:10px 12px;border-bottom:1px solid var(--border);text-align:left}
67
+ th{color:var(--sky);background:#0c1426;position:sticky;top:0}
68
+ tr:hover{background:#0c1426}
69
+ .flag{font-weight:700}.flag.red{color:var(--red)}.flag.amber{color:var(--amber)}.flag.green{color:var(--green)}
70
+
71
+ /* Charts */
72
+ .chart{height:380px} canvas{width:100%!important;height:100%!important}
73
+
74
+ /* Slider & Presets */
75
+ .slider-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:10px 0}
76
+ .chip{display:inline-block;padding:6px 10px;border-radius:999px;border:1px solid var(--border);background:#0b162c;color:#cbd5e1;font-size:12px;margin-right:6px}
77
+ .chip.good{color:var(--green)}.chip.watch{color:var(--amber)}.chip.low{color:var(--red)}
78
+ .preset{cursor:pointer;border:1px solid var(--border);background:#0b162c;padding:6px 10px;border-radius:999px;color:#cbd5e1;font-size:12px}
79
+ .preset.active{background:linear-gradient(90deg,var(--teal),var(--blue));color:#fff;border:none}
80
+ .foot{font-size:12px;color:var(--muted)}
81
+ @media (max-width:900px){.span-4,.span-8,.span-12{grid-column:span 12}}
82
+
83
+ /* Heatmap */
84
+ .heat-wrap{overflow:auto;border:1px solid var(--border);border-radius:12px}
85
+ .heat-table{border-collapse:separate;border-spacing:6px;width:max-content;min-width:100%;background:#0b1325}
86
+ .heat-table thead th{
87
+ position:sticky; top:0; z-index:2;
88
+ background:#0c1426; color:#cde9ff; font-weight:700; padding:10px 8px; border-radius:8px;
89
+ }
90
+ .heat-table tbody th{
91
+ position:sticky; left:0; z-index:1;
92
+ background:#0c1426; color:#e5eefc; font-weight:600; padding:10px 8px; border-radius:8px; white-space:nowrap;
93
+ }
94
+ .hm-cell{
95
+ min-width:74px; text-align:center; font-weight:700; color:white; padding:10px 6px; border-radius:10px;
96
+ box-shadow:inset 0 0 0 1px rgba(0,0,0,.25);
97
+ }
98
+ .hm-red{background:rgba(239,68,68,.9)}
99
+ .hm-amber{background:rgba(250,204,21,.92); color:#1b1b1b}
100
+ .hm-green{background:rgba(34,197,94,.9)}
101
+ .hm-empty{background:#0c1426; color:#64748b; font-weight:600}
102
+ .heat-legend{display:flex;gap:12px;align-items:center;margin-top:8px;color:var(--muted);font-size:12px}
103
+ .leg-chip{width:20px;height:14px;border-radius:4px;display:inline-block}
104
+
105
+ /* Sparklines */
106
+ .spark{display:block;width:120px;height:26px}
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <header>
111
+ <div class="brand">
112
+ <div class="brand-icon" aria-hidden="true">
113
+ <svg viewBox="0 0 24 24" fill="none">
114
+ <path d="M4 17 L10 11 L14 15 L21 8" stroke="#dbeafe" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
115
+ <circle cx="21" cy="8" r="1.6" fill="#dbeafe"/>
116
+ </svg>
117
+ </div>
118
+ <div class="brand-text">
119
+ <div class="brand-title">Curve Master Productivity Monitor</div>
120
+ <div class="brand-sub">Advanced Productivity Monitor</div>
121
+ </div>
122
+ </div>
123
+ <div class="hdr-actions">
124
+ <button id="exportPDF" class="btn">Export PDF + PNG + CSV</button>
125
+ <button id="clearData" class="btn btn-danger">Reset</button>
126
+ </div>
127
+ </header>
128
+
129
+ <main>
130
+ <!-- Upload + Summary (left), Filters (right) -->
131
+ <section class="grid">
132
+ <div class="card span-4" style="display:flex;flex-direction:column;gap:12px">
133
+ <h2>Upload Daily CSV</h2>
134
+ <div class="uploader">
135
+ <input id="fileInput" type="file" accept=".csv" />
136
+ <div class="sub">Headers (aliases OK): Date, Contractor, EarnedHrs, ActualHrs</div>
137
+ </div>
138
+
139
+ <div>
140
+ <h2 style="margin-bottom:6px">Import Summary</h2>
141
+ <div id="summary" class="sub">No data yet.</div>
142
+ <div id="importLog" class="sub"></div>
143
+ </div>
144
+ </div>
145
+
146
+ <div class="card span-8">
147
+ <h2>Filters</h2>
148
+ <div style="display:grid;grid-template-columns:1fr;gap:10px">
149
+ <div>
150
+ <label>Contractor</label>
151
+ <input id="contractorSearch" type="text" placeholder="Search contractors…" />
152
+ <div id="contractorBox"></div>
153
+ <div class="controls-row" style="margin-top:6px">
154
+ <button id="selectAll" class="pill">Select All</button>
155
+ <button id="clearPick" class="pill">Clear</button>
156
+ <div class="tog"><input type="checkbox" id="excludeSundays" checked><label for="excludeSundays">Exclude Sundays</label></div>
157
+ <div class="tog"><input type="checkbox" id="excludeToday" checked><label for="excludeToday">Exclude Today from rolling</label></div>
158
+ <button id="clearFilters" class="pill">Reset Filters</button>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </section>
164
+
165
+ <!-- League -->
166
+ <section class="grid" style="margin-top:16px">
167
+ <div class="card span-12" id="leagueCard">
168
+ <h2>Contractor Performance</h2>
169
+ <div class="sub">Sorted by worst 7-Day Avg CPI. Filters apply.</div>
170
+ <div id="leagueScroll" style="max-height:360px;overflow:auto">
171
+ <table id="leagueTable">
172
+ <thead>
173
+ <tr>
174
+ <th>Contractor</th>
175
+ <th>7-Day Avg CPI</th>
176
+ <th>5-Day Avg Earned</th>
177
+ <th>5-Day Avg Burn</th>
178
+ <th>Cum CPI</th>
179
+ <th>Trend (7–10d)</th>
180
+ <th>Flags</th>
181
+ </tr>
182
+ </thead>
183
+ <tbody></tbody>
184
+ </table>
185
+ </div>
186
+ </div>
187
+ </section>
188
+
189
+ <!-- Rolling CPI -->
190
+ <section class="grid" style="margin-top:16px">
191
+ <div class="card span-12" id="rollingCard">
192
+ <h2>Rolling CPI (Avg of Selected Contractors)</h2>
193
+ <div class="sub"><b>Target = 1.00</b>; green line shows the visible average.</div>
194
+ <div class="slider-row">
195
+ <span>Presets:</span>
196
+ <button class="preset" data-preset="3">3d</button>
197
+ <button class="preset" data-preset="5">5d</button>
198
+ <button class="preset" data-preset="7">7d</button>
199
+ <button class="preset" data-preset="10">10d</button>
200
+ <button class="preset" data-preset="all">All</button>
201
+ <span style="margin-left:16px">Look-back:</span>
202
+ <input id="lookback" type="range" min="3" max="30" value="7"/>
203
+ <span id="lookbackLabel" class="sub">7 days</span>
204
+ </div>
205
+ <div class="chart"><canvas id="rollingChart"></canvas></div>
206
+ <div id="perfChips" style="margin-top:8px"></div>
207
+ </div>
208
+ </section>
209
+
210
+ <!-- Projection -->
211
+ <section class="grid" style="margin-top:16px">
212
+ <div class="card span-12" id="projCard">
213
+ <h2>CPI Projection
214
+
215
+ </h2>
216
+ <div class="sub">Estimates future daily CPI from recent behavior of the selected contractors. Bands show 10–90% and 25–75% percentiles; line is the median path.</div>
217
+ <div class="controls-row" style="margin-bottom:6px">
218
+ <label>Target CPI</label>
219
+ <input id="projTarget" class="num" type="number" step="0.01" value="1.00"/>
220
+ <label>Horizon (days)</label>
221
+ <input id="projHorizon" class="num" type="number" min="15" max="120" step="5" value="90"/>
222
+ <label>Model lookback (days)</label>
223
+ <input id="projLookback" class="num" type="number" min="10" max="60" step="5" value="30"/>
224
+ <span class="sub">Uses most recent CPI dates; Sundays honored by your filter.</span>
225
+ </div>
226
+ <div class="chart"><canvas id="projChart"></canvas></div>
227
+ <div id="projStats" class="sub" style="margin-top:6px"></div>
228
+ </div>
229
+ </section>
230
+
231
+ <!-- Cumulative -->
232
+ <section class="grid" style="margin-top:16px">
233
+ <div class="card span-12" id="cumCard">
234
+ <h2>Cumulative Earned vs Actual</h2>
235
+ <div class="chart"><canvas id="cumChart"></canvas></div>
236
+ <div class="foot">Cumulative across filtered contractors & dates.</div>
237
+ </div>
238
+ </section>
239
+
240
+ <!-- Daily CPI Heatmap -->
241
+ <section class="grid" style="margin-top:16px">
242
+ <div class="card span-12" id="heatCard">
243
+ <h2>Daily CPI Heatmap</h2>
244
+ <div class="heat-wrap" id="heatWrap">
245
+ <table class="heat-table" id="heatTable"></table>
246
+ </div>
247
+ <div class="heat-legend">
248
+ <span class="leg-chip" style="background:rgba(239,68,68,.9)"></span> &lt; 0.80
249
+ <span class="leg-chip" style="background:rgba(250,204,21,.92)"></span> 0.80–0.99
250
+ <span class="leg-chip" style="background:rgba(34,197,94,.9)"></span> ≥ 1.00
251
+ </div>
252
+ </div>
253
+ </section>
254
+ </main>
255
+
256
+ <!-- Libs -->
257
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
258
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.10/dayjs.min.js"></script>
259
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
260
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
261
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
262
+
263
+ <script>
264
+ /* ===== STATE ===== */
265
+ const state = {
266
+ rows: [],
267
+ filters: { contractors:[], excludeSundays:true, excludeTodayInRolling:true },
268
+ charts: { cum:null, rolling:null, proj:null },
269
+ lookbackDays: 'all'
270
+ };
271
+
272
+ const HEADER_ALIASES = {
273
+ Date:['date','workdate','reportdate'],
274
+ Contractor:['contractor','company','vendor'],
275
+ EarnedHrs:['earnedhrs','earned','evhrs','scheduleearn'],
276
+ ActualHrs:['actualhrs','actual','paidhours','paidhrs','timesheethrs','timesheet']
277
+ };
278
+
279
+ /* ===== HELPERS ===== */
280
+ const el = id => document.getElementById(id);
281
+ const toISO = v => { const d = dayjs(v); return d.isValid()? d.format('YYYY-MM-DD'):null; };
282
+ const isSunday = iso => dayjs(iso).day()===0;
283
+ const normalizeHeader=(h)=>{ if(!h) return null; const k=h.toString().trim().toLowerCase();
284
+ for(const canon in HEADER_ALIASES){ const set=[canon.toLowerCase(),...HEADER_ALIASES[canon]]; if(set.includes(k)) return canon; } return null; };
285
+
286
+ // BLANKS -> null, numbers left as numbers; zeros are kept as 0 (not null)
287
+ const num = x => {
288
+ if (x === undefined || x === null || String(x).trim() === '') return null;
289
+ const n = Number(String(x).replace(/,/g,'').trim());
290
+ return Number.isFinite(n) ? n : null;
291
+ };
292
+
293
+ /* ===== IMPORT ===== */
294
+ el('fileInput').addEventListener('change', (e)=>{
295
+ const f=e.target.files?.[0]; if(!f) return;
296
+ Papa.parse(f,{header:true,skipEmptyLines:true,complete:(res)=>{
297
+ const headerMap={}; for(const h of res.meta.fields){ const canon=normalizeHeader(h); if(canon) headerMap[canon]=h; }
298
+ for(const must of ['Date','Contractor','EarnedHrs','ActualHrs']) if(!headerMap[must]){ el('importLog').innerHTML='<span style="color:#ef4444">Missing required headers.</span>'; return; }
299
+ const rows=[];
300
+ for(const r of res.data){
301
+ const d=toISO(r[headerMap.Date]); const c=(r[headerMap.Contractor]||'').trim();
302
+ const e=num(r[headerMap.EarnedHrs]); const a=num(r[headerMap.ActualHrs]);
303
+ if(!d || !c) continue; rows.push({Date:d, Contractor:c, EarnedHrs:e, ActualHrs:a});
304
+ }
305
+ state.rows.push(...rows);
306
+ el('importLog').textContent = `Imported ${rows.length} rows.`;
307
+ buildContractorList(); renderAll();
308
+ }});
309
+ });
310
+
311
+ /* ===== HEADER ACTIONS (EXPORT) ===== */
312
+ document.getElementById('clearData').addEventListener('click', ()=>{
313
+ state.rows=[]; state.filters={ contractors:[], excludeSundays:true, excludeTodayInRolling:true };
314
+ el('contractorSearch').value=''; el('importLog').textContent='Cleared all data.'; buildContractorList(); destroyCharts(); renderAll();
315
+ });
316
+
317
+ document.getElementById('exportPDF').addEventListener('click', async ()=>{
318
+ const { jsPDF } = window.jspdf;
319
+
320
+ const ts = dayjs().format('YYYY-MM-DD_HH-mm');
321
+ const runLabel = `Run: ${dayjs().format('YYYY-MM-DD HH:mm')}`;
322
+ const titleTxt = 'Curve Master Productivity Monitor';
323
+
324
+ const doc = new jsPDF({orientation:'landscape', unit:'pt', format:'letter'}); // 792x612
325
+ const pageW = doc.internal.pageSize.getWidth();
326
+ const pageH = doc.internal.pageSize.getHeight();
327
+ const M = 24, headerH = 44;
328
+
329
+ function drawHeader(){
330
+ doc.setFillColor(12,21,38); doc.rect(0,0,pageW,headerH,'F');
331
+ doc.setFont('helvetica','bold'); doc.setFontSize(16); doc.setTextColor(219,234,254);
332
+ doc.text(titleTxt, M, 28);
333
+ doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(185,200,255);
334
+ const w = doc.getTextWidth(runLabel); doc.text(runLabel, pageW-M-w, 28);
335
+ }
336
+
337
+ drawHeader();
338
+ let y = M + headerH;
339
+
340
+ async function addCard(selector, before, after){
341
+ const node = document.querySelector(selector); if(!node) return;
342
+ if(before) before();
343
+ const canvas = await html2canvas(node, { backgroundColor:'#0f172a', useCORS:true, scale:2 });
344
+ if(after) after();
345
+ const img = canvas.toDataURL('image/png');
346
+ const drawW = pageW - 2*M;
347
+ const drawH = canvas.height * (drawW / canvas.width);
348
+ if (y + drawH > pageH - M){ doc.addPage(); drawHeader(); y = M + headerH; }
349
+ doc.addImage(img, 'PNG', M, y, drawW, drawH);
350
+ y += drawH + 12;
351
+ }
352
+
353
+ const expandLeague = ()=>{ const sc = document.getElementById('leagueScroll'); if(!sc) return; sc._prev={h:sc.style.maxHeight,o:sc.style.overflow}; sc.style.maxHeight='none'; sc.style.overflow='visible'; };
354
+ const restoreLeague= ()=>{ const sc = document.getElementById('leagueScroll'); if(sc&&sc._prev){ sc.style.maxHeight=sc._prev.h; sc.style.overflow=sc._prev.o; sc._prev=null; } };
355
+ const expandHeat = ()=>{ const hw = document.getElementById('heatWrap'); if(!hw) return; hw._prev={o:hw.style.overflow}; hw.style.overflow='visible'; };
356
+ const restoreHeat = ()=>{ const hw = document.getElementById('heatWrap'); if(hw&&hw._prev){ hw.style.overflow=hw._prev.o; hw._prev=null; } };
357
+
358
+ await addCard('#leagueCard', expandLeague, restoreLeague);
359
+ await addCard('#rollingCard');
360
+ await addCard('#projCard');
361
+ await addCard('#cumCard');
362
+ await addCard('#heatCard', expandHeat, restoreHeat);
363
+
364
+ doc.save(`CurveMaster_Report_${ts}.pdf`);
365
+
366
+ // Extras
367
+ const heatCard = document.querySelector('#heatWrap');
368
+ if (heatCard){
369
+ const heatCanvas = await html2canvas(heatCard, { backgroundColor:'#0f172a', useCORS:true, scale:1.5 });
370
+ const link = document.createElement('a');
371
+ link.href = heatCanvas.toDataURL('image/png');
372
+ link.download = `CurveMaster_Heatmap_${ts}.png`;
373
+ link.click();
374
+ }
375
+
376
+ const csv = buildMetricsCSV();
377
+ if (csv){
378
+ const blob = new Blob(["\ufeff"+csv], {type: "text/csv;charset=utf-8;"});
379
+ const link = document.createElement('a');
380
+ link.href = URL.createObjectURL(blob);
381
+ link.download = `CurveMaster_Metrics_${ts}.csv`;
382
+ link.click();
383
+ }
384
+ });
385
+
386
+ function destroyCharts(){
387
+ if(state.charts.cum){state.charts.cum.destroy();state.charts.cum=null}
388
+ if(state.charts.rolling){state.charts.rolling.destroy();state.charts.rolling=null}
389
+ if(state.charts.proj){state.charts.proj.destroy();state.charts.proj=null}
390
+ }
391
+
392
+ /* ===== CONTRACTOR PICKER ===== */
393
+ function buildContractorList(){
394
+ const box=el('contractorBox'); const q=el('contractorSearch').value?.toLowerCase()||'';
395
+ const names=[...new Set(state.rows.map(r=>r.Contractor))].sort().filter(n=>n.toLowerCase().includes(q));
396
+ const sel=new Set(state.filters.contractors);
397
+ box.innerHTML = names.map(n=>`<label class="c-item"><input type="checkbox" value="${n.replace(/"/g,'&quot;')}" ${sel.has(n)?'checked':''}/> ${n}</label>`).join('') || '<div class="sub">No contractors</div>';
398
+ box.querySelectorAll('input[type="checkbox"]').forEach(cb=>{
399
+ cb.addEventListener('change', ()=>{
400
+ const set=new Set(state.filters.contractors);
401
+ if(cb.checked) set.add(cb.value); else set.delete(cb.value);
402
+ state.filters.contractors=[...set]; renderAll();
403
+ });
404
+ });
405
+ }
406
+ el('contractorSearch').addEventListener('input', buildContractorList);
407
+ el('selectAll').addEventListener('click',()=>{ const names=[...new Set(state.rows.map(r=>r.Contractor))]; state.filters.contractors=names; buildContractorList(); renderAll(); });
408
+ el('clearPick').addEventListener('click',()=>{ state.filters.contractors=[]; buildContractorList(); renderAll(); });
409
+
410
+ /* ===== FILTERED DATA & METRICS ===== */
411
+ function filteredRows(){
412
+ let rows=[...state.rows];
413
+ if(state.filters.contractors.length){ const set=new Set(state.filters.contractors); rows=rows.filter(r=>set.has(r.Contractor)); }
414
+ if(state.filters.excludeSundays) rows=rows.filter(r=>!isSunday(r.Date));
415
+ return rows.sort((a,b)=>a.Date.localeCompare(b.Date)||a.Contractor.localeCompare(b.Contractor));
416
+ }
417
+
418
+ // CPI: if Actual missing or 0 -> null; else (Earned null treated as 0) / Actual
419
+ const calcDaily = r => {
420
+ const E = r.EarnedHrs;
421
+ const A = r.ActualHrs;
422
+ const CPI = (A == null || A === 0) ? null : ((E ?? 0) / A);
423
+ return { ...r, CPI, BurnHrs: A };
424
+ };
425
+
426
+ function groupByContractor(rows){
427
+ const m=new Map();
428
+ for(const r of rows){ if(!m.has(r.Contractor)) m.set(r.Contractor,[]); m.get(r.Contractor).push(r); }
429
+ for(const v of m.values()) v.sort((a,b)=>a.Date.localeCompare(b.Date));
430
+ return m;
431
+ }
432
+
433
+ // Per-day aggregation keeping zeros; whole-day missing -> null
434
+ function daysFor(rows){
435
+ const by = new Map();
436
+ for (const r of rows){
437
+ const d = r.Date;
438
+ if (!by.has(d)) by.set(d, { Date:d, EarnedHrs:0, BurnHrs:0, hasE:false, hasB:false });
439
+ const agg = by.get(d);
440
+ if (r.EarnedHrs != null) { agg.EarnedHrs += r.EarnedHrs; agg.hasE = true; }
441
+ if (r.BurnHrs != null) { agg.BurnHrs += r.BurnHrs; agg.hasB = true; }
442
+ }
443
+ return [...by.values()]
444
+ .sort((a,b)=>a.Date.localeCompare(b.Date))
445
+ .map(d=>({
446
+ Date:d.Date,
447
+ EarnedHrs: d.hasE ? d.EarnedHrs : null,
448
+ BurnHrs: d.hasB ? d.BurnHrs : null
449
+ }));
450
+ }
451
+ function avgLastNDates(rows, n, key){
452
+ const days = daysFor(rows);
453
+ const last = days.slice(-n).map(d=>d[key]).filter(v=>v!=null); // keep zeros, drop nulls
454
+ if (!last.length) return null;
455
+ return last.reduce((a,b)=>a+b,0) / last.length;
456
+ }
457
+
458
+ function rollingAvg(series,days,{excludeToday=false}={}){
459
+ if(!series.length) return null; const last=series[series.length-1]; const end=dayjs(last.Date); const eff=excludeToday?end.subtract(1,'day'):end; const start=eff.subtract(days-1,'day');
460
+ const w=series.filter(p=>{const d=dayjs(p.Date); return d.isAfter(start.subtract(1,'day'))&&d.isBefore(eff.add(1,'day'))&&p.value!=null;});
461
+ if(!w.length) return null; return w.reduce((a,p)=>a+Number(p.value),0)/w.length;
462
+ }
463
+ const sevenDayAvgCPI = rows => rollingAvg(rows.map(r=>({Date:r.Date,value:r.CPI})),7);
464
+
465
+ function cumulativeByDate(rows){
466
+ const map={};
467
+ for(const r of rows){
468
+ map[r.Date]??={E:0,A:0};
469
+ map[r.Date].E += (r.EarnedHrs ?? 0);
470
+ map[r.Date].A += (r.ActualHrs ?? 0);
471
+ }
472
+ let e=0,a=0;
473
+ return Object.keys(map).sort().map(d=>{ e+=map[d].E; a+=map[d].A; return {Date:d,CumEarned:e,CumActual:a}; });
474
+ }
475
+ function cumulativeCPI(rows){
476
+ let e=0,a=0;
477
+ for(const r of rows){ e += (r.EarnedHrs ?? 0); a += (r.ActualHrs ?? 0); }
478
+ return a ? e/a : null;
479
+ }
480
+
481
+ /* ===== Chart helpers (crosshair + horizontal lines) ===== */
482
+ const crosshairPlugin={ id:'cmCross', afterDatasetsDraw(chart,args,opt){ const act=chart.tooltip?.getActiveElements?.(); if(!act||!act.length) return; const {ctx,chartArea,scales:{x}}=chart; const i=act[0].index; const px=x.getPixelForValue(i); ctx.save(); ctx.strokeStyle=opt?.color||'#38bdf8'; ctx.setLineDash([4,3]); ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(px,chartArea.top); ctx.lineTo(px,chartArea.bottom); ctx.stroke(); ctx.restore(); } };
483
+ const hlinePlugin={ id:'cmH', afterDatasetsDraw(chart,args,opt){ const lines=opt?.lines||[]; const {ctx,chartArea,scales:{y}}=chart; ctx.save(); lines.forEach(l=>{ const ypx=y.getPixelForValue(l.value); ctx.strokeStyle=l.color||'#94a3b8'; ctx.setLineDash([6,4]); ctx.lineWidth=1.25; ctx.beginPath(); ctx.moveTo(chartArea.left,ypx); ctx.lineTo(chartArea.right,ypx); ctx.stroke(); if(l.label){ ctx.fillStyle=l.color||'#94a3b8'; ctx.font='12px Arial'; ctx.fillText(l.label, chartArea.left+6, ypx-6);} }); ctx.restore(); } };
484
+ Chart.register(crosshairPlugin,hlinePlugin);
485
+
486
+ /* ===== League (with sparklines) ===== */
487
+ function lastNDailyCPI(rows, n=10){
488
+ const arr = rows.map(r=>r.CPI).filter(v=>v!=null && isFinite(v));
489
+ return arr.slice(-n);
490
+ }
491
+ function renderLeague(){
492
+ const tbody=document.querySelector('#leagueTable tbody'); tbody.innerHTML='';
493
+ const byC=groupByContractor(filteredRows().map(calcDaily));
494
+ const out=[];
495
+ for(const [name,rows] of byC){
496
+ const c7=sevenDayAvgCPI(rows);
497
+ const e5=avgLastNDates(rows,5,'EarnedHrs');
498
+ const b5=avgLastNDates(rows,5,'BurnHrs');
499
+ const cum=cumulativeCPI(rows);
500
+ const trend = lastNDailyCPI(rows, 10);
501
+ let flag=''; if(c7==null) flag='<span class="flag amber">No data</span>'; else if(c7<0.80) flag='<span class="flag red">🔴 Low</span>'; else if(c7<1.00) flag='<span class="flag amber">🟡 Watch</span>'; else flag='<span class="flag green">🟢 Good</span>';
502
+ out.push({
503
+ name,
504
+ c7: c7==null?'-':c7.toFixed(2),
505
+ e5: e5==null?'-':Math.round(e5).toLocaleString(),
506
+ b5: b5==null?'-':Math.round(b5).toLocaleString(),
507
+ cum: cum==null?'-':cum.toFixed(2),
508
+ trend,
509
+ flag
510
+ });
511
+ }
512
+ out.sort((a,b)=>(a.c7==='-'?999:+a.c7)-(b.c7==='-'?999:+b.c7));
513
+ for(const r of out){
514
+ const tr=document.createElement('tr');
515
+ const sparkId = 'spk_'+Math.random().toString(36).slice(2);
516
+ tr.innerHTML=
517
+ `<td>${r.name}</td>
518
+ <td>${r.c7}</td>
519
+ <td>${r.e5}</td>
520
+ <td>${r.b5}</td>
521
+ <td>${r.cum}</td>
522
+ <td><canvas class="spark" id="${sparkId}" width="120" height="26" data-points="${encodeURIComponent(JSON.stringify(r.trend))}"></canvas></td>
523
+ <td>${r.flag}</td>`;
524
+ tbody.appendChild(tr);
525
+ }
526
+ renderSparklines();
527
+ }
528
+
529
+ function renderSparklines(){
530
+ document.querySelectorAll('canvas.spark').forEach(cv=>{
531
+ const pts = JSON.parse(decodeURIComponent(cv.getAttribute('data-points')||'[]'));
532
+ const ctx = cv.getContext('2d'); const w=cv.width, h=cv.height; ctx.clearRect(0,0,w,h);
533
+ if(!pts.length){ ctx.fillStyle='#64748b'; ctx.font='12px Arial'; ctx.fillText('—', w/2-3, h/2+4); return; }
534
+ let min = Math.min(...pts, 1.0), max = Math.max(...pts, 1.0);
535
+ if(min===max){ min-=0.1; max+=0.1; }
536
+ const padY=3, padX=2;
537
+ const x = i => padX + (w-2*padX) * (i/(pts.length-1||1));
538
+ const y = v => padY + (h-2*padY) * (1 - (v-min)/(max-min));
539
+ ctx.strokeStyle='rgba(239,68,68,.8)'; ctx.setLineDash([4,3]); ctx.beginPath();
540
+ const yT = y(1.0); ctx.moveTo(padX, yT); ctx.lineTo(w-padX, yT); ctx.stroke(); ctx.setLineDash([]);
541
+ ctx.strokeStyle='#38bdf8'; ctx.lineWidth=1.5; ctx.beginPath();
542
+ pts.forEach((v,i)=>{ const xi=x(i), yi=y(v); i?ctx.lineTo(xi,yi):ctx.moveTo(xi,yi); });
543
+ ctx.stroke();
544
+ const lastY = y(pts[pts.length-1]), lastX = x(pts.length-1);
545
+ ctx.fillStyle='#22c55e'; ctx.beginPath(); ctx.arc(lastX,lastY,2.2,0,Math.PI*2); ctx.fill();
546
+ });
547
+ }
548
+
549
+ /* ===== Cumulative ===== */
550
+ function renderCumChart(){
551
+ const ctx=el('cumChart'); const series=cumulativeByDate(filteredRows());
552
+ const labels=series.map(p=>p.Date), e=series.map(p=>p.CumEarned), a=series.map(p=>p.CumActual);
553
+ if(state.charts.cum) state.charts.cum.destroy();
554
+ state.charts.cum=new Chart(ctx,{type:'line',data:{labels,datasets:[{label:'Cum Earned',data:e,tension:.25,borderWidth:2},{label:'Cum Actual',data:a,tension:.25,borderWidth:2}]},
555
+ options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
556
+ plugins:{legend:{labels:{color:'#cbd5e1'}},tooltip:{mode:'index',intersect:false}},
557
+ scales:{x:{ticks:{color:'#94a3b8'},grid:{color:'rgba(148,163,184,.15)'}},y:{ticks:{color:'#94a3b8'},grid:{color:'rgba(148,163,184,.15)'}}}}});
558
+ }
559
+
560
+ /* ===== Rolling CPI ===== */
561
+ function avgDailyCPI(){
562
+ const byC=groupByContractor(filteredRows().map(calcDaily)); const byDate={};
563
+ for(const arr of byC.values()){ for(const r of arr){ if(r.CPI==null) continue; (byDate[r.Date]??=[]).push(r.CPI); } }
564
+ return Object.keys(byDate).sort().map(d=>({Date:d,value:byDate[d].reduce((a,x)=>a+x,0)/byDate[d].length}));
565
+ }
566
+ function avgLast(series,n){ const s=series.slice(-n); if(!s.length) return null; return s.reduce((a,p)=>a+p.value,0)/s.length; }
567
+ function chipClass(v){ if(v==null) return 'chip'; if(v<0.80) return 'chip low'; if(v<1.00) return 'chip watch'; return 'chip good'; }
568
+
569
+ function renderRollingChart(){
570
+ const ctx=el('rollingChart'); const series=avgDailyCPI(); const dates=series.map(s=>s.Date); const vals=series.map(s=>+s.value.toFixed(3));
571
+ let startIdx=0; if(state.lookbackDays!=='all'){ const n=+state.lookbackDays; startIdx=Math.max(0,vals.length-n); }
572
+ const visDates=dates.slice(startIdx), visVals=vals.slice(startIdx);
573
+
574
+ const nonNull=visVals.filter(v=>v!=null); const visAvg=nonNull.length? nonNull.reduce((a,b)=>a+b,0)/nonNull.length : null;
575
+ const vMin = Math.min(...nonNull, 1.00, ...(visAvg?[visAvg]:[]));
576
+ const vMax = Math.max(...nonNull, 1.00, ...(visAvg?[visAvg]:[]));
577
+ const pad = Math.max(0.05, (vMax - vMin) * 0.15);
578
+
579
+ if(state.charts.rolling) state.charts.rolling.destroy();
580
+ state.charts.rolling=new Chart(ctx,{
581
+ type:'line',
582
+ data:{labels:visDates,datasets:[{label:'Rolling CPI (daily avg of selected)',data:visVals,tension:.25,borderWidth:2}]},
583
+ options:{
584
+ responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
585
+ plugins:{
586
+ legend:{labels:{color:'#cbd5e1'}},
587
+ tooltip:{mode:'index',intersect:false},
588
+ cmCross:{ color:'#38bdf8' },
589
+ cmH:{ lines:[
590
+ { value:1.0, color:'#ef4444', label:'Target 1.00' },
591
+ ...(visAvg!=null ? [{ value:visAvg, color:'#22c55e', label:`Avg ${visAvg.toFixed(2)}` }] : [])
592
+ ]}
593
+ },
594
+ scales:{
595
+ x:{ticks:{color:'#94a3b8',autoSkip:true,maxRotation:0},grid:{color:'rgba(148,163,184,.12)'}},
596
+ y:{ticks:{color:'#94a3b8'},grid:{color:'rgba(148,163,184,.12)'},
597
+ suggestedMin: vMin - pad, suggestedMax: vMax + pad }
598
+ }
599
+ }
600
+ });
601
+
602
+ const perf=[3,5,7,10].map(n=>{ const v=avgLast(series,n); return `<span class="${chipClass(v)}">${n}-Day Avg CPI: <b>${v==null?'-':v.toFixed(2)}</b></span>`; }).join('');
603
+ el('perfChips').innerHTML=perf;
604
+
605
+ const slider=el('lookback'); const max=Math.max(3,dates.length); slider.max=max;
606
+ if(state.lookbackDays==='all'){ slider.value=Math.min(30,max); el('lookbackLabel').textContent='All data'; setPresetActive('all'); }
607
+ else { slider.value=state.lookbackDays; el('lookbackLabel').textContent=`${state.lookbackDays} days`; setPresetActive(state.lookbackDays); }
608
+ }
609
+
610
+ function setPresetActive(v){ document.querySelectorAll('.preset').forEach(b=>b.classList.toggle('active', b.dataset.preset==String(v))); }
611
+ el('lookback').addEventListener('input',e=>{ state.lookbackDays=+e.target.value; el('lookbackLabel').textContent=`${state.lookbackDays} days`; setPresetActive(state.lookbackDays); renderRollingChart(); });
612
+ document.querySelectorAll('.preset').forEach(b=>b.addEventListener('click',()=>{ state.lookbackDays=(b.dataset.preset==='all'?'all':+b.dataset.preset); renderRollingChart(); }));
613
+
614
+ /* ===== Projection (Monte Carlo) ===== */
615
+ function forwardDates(startISO, days, skipSundays=true){
616
+ const out=[]; let d=dayjs(startISO);
617
+ while(out.length<days){
618
+ d=d.add(1,'day');
619
+ if(skipSundays && d.day()===0) continue;
620
+ out.push(d.format('YYYY-MM-DD'));
621
+ }
622
+ return out;
623
+ }
624
+ function simulatePaths(startVal, mu, sigma, steps, sims){
625
+ const paths=new Array(sims);
626
+ for(let s=0;s<sims;s++){
627
+ const p=new Array(steps);
628
+ let v=startVal;
629
+ for(let i=0;i<steps;i++){
630
+ const u1=Math.random()||1e-9, u2=Math.random()||1e-9;
631
+ const z=Math.sqrt(-2*Math.log(u1))*Math.cos(2*Math.PI*u2);
632
+ v = v + mu + sigma*z; // additive on CPI
633
+ v = Math.max(0.2, Math.min(2.5, v));
634
+ p[i]=+v.toFixed(3);
635
+ }
636
+ paths[s]=p;
637
+ }
638
+ return paths;
639
+ }
640
+ function quantile(arr,q){
641
+ if(!arr.length) return null;
642
+ const sorted=[...arr].sort((a,b)=>a-b);
643
+ const idx=(sorted.length-1)*q;
644
+ const lo=Math.floor(idx), hi=Math.ceil(idx);
645
+ if(lo===hi) return sorted[lo];
646
+ return sorted[lo] + (sorted[hi]-sorted[lo])*(idx-lo);
647
+ }
648
+
649
+ function renderProjection(){
650
+ const target = parseFloat(el('projTarget').value)||1.0;
651
+ const horizon = Math.max(15, Math.min(120, parseInt(el('projHorizon').value)||90));
652
+ const lookback = Math.max(10, Math.min(60, parseInt(el('projLookback').value)||30));
653
+
654
+ const series = avgDailyCPI(); // [{Date, value}]
655
+ if(!series.length){
656
+ el('projStats').innerHTML = 'No CPI history available for projection.';
657
+ if(state.charts.proj){ state.charts.proj.destroy(); state.charts.proj=null; }
658
+ return;
659
+ }
660
+ const last = series[series.length-1];
661
+ const hist = series.slice(-lookback);
662
+ const deltas = [];
663
+ for(let i=1;i<hist.length;i++){ deltas.push( (hist[i].value - hist[i-1].value) ); }
664
+ const mu = deltas.length ? deltas.reduce((a,b)=>a+b,0)/deltas.length : 0;
665
+ const sd = deltas.length ? Math.sqrt(deltas.reduce((a,b)=>a+b*b,0)/deltas.length - mu*mu) : 0.05;
666
+
667
+ const steps = horizon, sims = 400;
668
+ const paths = simulatePaths(hist[hist.length-1].value, mu, sd, steps, sims);
669
+
670
+ const p10=[], p25=[], p50=[], p75=[], p90=[];
671
+ for(let i=0;i<steps;i++){
672
+ const col = paths.map(p=>p[i]);
673
+ p10.push(quantile(col,0.10));
674
+ p25.push(quantile(col,0.25));
675
+ p50.push(quantile(col,0.50));
676
+ p75.push(quantile(col,0.75));
677
+ p90.push(quantile(col,0.90));
678
+ }
679
+
680
+ const pastN = 30;
681
+ const past = series.slice(-pastN);
682
+ const forward = forwardDates(last.Date, steps, state.filters.excludeSundays);
683
+ const labels = past.map(p=>p.Date).concat(forward);
684
+
685
+ const ds = [];
686
+ ds.push({label:'Past CPI', data: past.map(p=>+p.value.toFixed(3)).concat(new Array(steps).fill(null)),
687
+ borderColor:'#22c55e', pointRadius:0, borderWidth:2, tension:.25});
688
+ const bandColor1='rgba(56,189,248,0.18)', bandColor2='rgba(56,189,248,0.32)';
689
+ const padNulls = arr => new Array(past.length).fill(null).concat(arr);
690
+ ds.push({label:'90th', data: padNulls(p90), pointRadius:0, borderWidth:0});
691
+ ds.push({label:'10-90% band', data: padNulls(p10), pointRadius:0, borderWidth:0,
692
+ fill:{ target:'-1', above:bandColor1, below:bandColor1 }});
693
+ ds.push({label:'75th', data: padNulls(p75), pointRadius:0, borderWidth:0});
694
+ ds.push({label:'25-75% band', data: padNulls(p25), pointRadius:0, borderWidth:0,
695
+ fill:{ target:'-1', above:bandColor2, below:bandColor2 }});
696
+ ds.push({label:'Median', data: padNulls(p50), borderColor:'#38bdf8', pointRadius:0, borderWidth:2, tension:.25});
697
+
698
+ if(state.charts.proj) state.charts.proj.destroy();
699
+ const ctx = el('projChart');
700
+ state.charts.proj = new Chart(ctx,{
701
+ type:'line',
702
+ data:{ labels, datasets: ds },
703
+ options:{
704
+ responsive:true, maintainAspectRatio:false, interaction:{ mode:'index', intersect:false },
705
+ plugins:{
706
+ legend:{ labels:{ color:'#cbd5e1' } },
707
+ tooltip:{ mode:'index', intersect:false },
708
+ cmCross:{ color:'#38bdf8' },
709
+ cmH:{ lines:[ { value:1.0, color:'#ef4444', label:'Target 1.00 (ref)' },
710
+ { value:target, color:'#facc15', label:`Target ${target.toFixed(2)}` } ] }
711
+ },
712
+ scales:{
713
+ x:{ ticks:{ color:'#94a3b8', autoSkip:true, maxRotation:0 }, grid:{ color:'rgba(148,163,184,.12)' } },
714
+ y:{ ticks:{ color:'#94a3b8' }, grid:{ color:'rgba(148,163,184,.12)' } }
715
+ }
716
+ }
717
+ });
718
+
719
+ const lastCol = paths.map(p=>p[p.length-1]);
720
+ const prob = lastCol.filter(v=>v>=target).length / lastCol.length;
721
+ const q10 = quantile(lastCol,0.10), q50 = quantile(lastCol,0.50), q90 = quantile(lastCol,0.90);
722
+
723
+ el('projStats').innerHTML =
724
+ `Horizon: <b>${horizon} days</b> · Lookback: <b>${lookback}</b> · Drift: <b>${mu.toFixed(3)}</b> · Vol: <b>${sd.toFixed(3)}</b><br>
725
+ P(CPI ≥ ${target.toFixed(2)}) ≈ <b>${(prob*100).toFixed(0)}%</b> · Last-day CPI ~ P10 <b>${q10.toFixed(2)}</b> | Median <b>${q50.toFixed(2)}</b> | P90 <b>${q90.toFixed(2)}</b>`;
726
+ }
727
+ ['projTarget','projHorizon','projLookback'].forEach(id=>{
728
+ el(id).addEventListener('input', renderProjection);
729
+ el(id).addEventListener('change', renderProjection);
730
+ });
731
+
732
+ /* ===== Heatmap ===== */
733
+ function cpiClass(v){
734
+ if(v==null || !isFinite(v)) return 'hm-empty';
735
+ if(v < 0.80) return 'hm-red';
736
+ if(v < 1.00) return 'hm-amber';
737
+ return 'hm-green';
738
+ }
739
+ function renderHeatmap(){
740
+ const table = document.getElementById('heatTable');
741
+ if(!table) return;
742
+ const rows = filteredRows().map(calcDaily);
743
+ if(rows.length===0){
744
+ table.innerHTML = '<thead><tr><th>Contractor</th></tr></thead><tbody><tr><td class="sub" style="padding:14px">No data.</td></tr></tbody>';
745
+ return;
746
+ }
747
+ const contractors = [...new Set(rows.map(r=>r.Contractor))].sort();
748
+ const dates = [...new Set(rows.map(r=>r.Date))].sort();
749
+
750
+ const key = (c,d)=>`${c}__${d}`;
751
+ const map = new Map();
752
+ for(const r of rows){ map.set(key(r.Contractor,r.Date), r.CPI); }
753
+
754
+ let thead = '<thead><tr><th>Contractor</th>';
755
+ for(const d of dates){ thead += `<th>${d}</th>`; }
756
+ thead += '</tr></thead>';
757
+
758
+ let tbody = '<tbody>';
759
+ for(const c of contractors){
760
+ tbody += `<tr><th>${c}</th>`;
761
+ for(const d of dates){
762
+ const v = map.get(key(c,d));
763
+ const cls = cpiClass(v);
764
+ const txt = (v==null || !isFinite(v)) ? '—' : v.toFixed(2);
765
+ const title = (v==null || !isFinite(v)) ? `${c} • ${d}: no data` : `${c} • ${d}: CPI ${v.toFixed(3)}`;
766
+ tbody += `<td class="hm-cell ${cls}" title="${title}">${txt}</td>`;
767
+ }
768
+ tbody += '</tr>';
769
+ }
770
+ tbody += '</tbody>';
771
+
772
+ table.innerHTML = thead + tbody;
773
+ }
774
+
775
+ /* ===== Metrics CSV for export ===== */
776
+ function buildMetricsCSV(){
777
+ const byC=groupByContractor(filteredRows().map(calcDaily));
778
+ const rows = [['Contractor','7DayAvgCPI','5DayAvgEarned','5DayAvgBurn','CumCPI']];
779
+ for(const [name,list] of byC){
780
+ const c7=sevenDayAvgCPI(list);
781
+ const e5=avgLastNDates(list,5,'EarnedHrs');
782
+ const b5=avgLastNDates(list,5,'BurnHrs');
783
+ const cum=cumulativeCPI(list);
784
+ rows.push([
785
+ name,
786
+ c7==null?'':c7.toFixed(3),
787
+ e5==null?'':Math.round(e5),
788
+ b5==null?'':Math.round(b5),
789
+ cum==null?'':cum.toFixed(3)
790
+ ]);
791
+ }
792
+ return rows.map(r=>r.map(v=>String(v).replace(/"/g,'""')).map(v=>`"${v}"`).join(',')).join('\r\n');
793
+ }
794
+
795
+ /* ===== Summary + Render ===== */
796
+ function renderSummary(){
797
+ const rows=filteredRows();
798
+ const dates=[...new Set(rows.map(r=>r.Date))].sort();
799
+ const cum=cumulativeCPI(rows);
800
+ el('summary').innerHTML = `
801
+ <div>Rows: <b>${rows.length}</b></div>
802
+ <div>Contractors: <b>${new Set(rows.map(r=>r.Contractor)).size}</b></div>
803
+ <div>Dates: <b>${dates.length}</b> <span class="sub">(${dates[0]||'-'} → ${dates[dates.length-1]||'-'})</span></div>
804
+ <div>Cumulative CPI: <b>${cum==null?'-':cum.toFixed(2)}</b></div>`;
805
+ }
806
+
807
+ function renderAll(){
808
+ renderLeague();
809
+ renderRollingChart();
810
+ renderProjection();
811
+ renderCumChart();
812
+ renderSummary();
813
+ renderHeatmap();
814
+ }
815
+
816
+ buildContractorList(); renderAll();
817
+ </script>
818
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=Kaliman-1981/prdtrend1-0" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
819
  </html>
prompts.txt ADDED
File without changes