Spaces:
Sleeping
Sleeping
| <html lang="en" data-theme="light"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TrackIQ Dashboard</title> | |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0/dist/chartjs-plugin-datalabels.min.js"></script> | |
| <style> | |
| :root{--bg:#0c1117;--s:#151c25;--s2:#1c2531;--bd:rgba(255,255,255,0.07);--sh:0 2px 16px rgba(0,0,0,0.45);--shlg:0 8px 36px rgba(0,0,0,0.55);--t:#e8edf2;--t2:#7a8fa0;--t3:#3a4a5a;--ac:#880000;--ac2:#660000;--acl:rgba(136,0,0,0.12);--gr:#16a34a;--grb:#052e16;--bl:#3b82f6;--blb:#0f2040;--yw:#f59e0b;--ywb:#1a0f00;--re:#ef4444;--reb:#2d0000;--sw:60px;--r:14px;--rs:9px;} | |
| [data-theme="light"]{--bg:#f0f4f8;--s:#fff;--s2:#f6f8fb;--bd:rgba(0,0,0,0.07);--sh:0 2px 16px rgba(0,0,0,0.09);--shlg:0 8px 36px rgba(0,0,0,0.13);--t:#0f1923;--t2:#5a6a7a;--t3:#9aaab8;--grb:#dcfce7;--blb:#dbeafe;--ywb:#fef9c3;--reb:#fee2e2;} | |
| *{box-sizing:border-box;margin:0;padding:0}::-webkit-scrollbar{width:0;height:0} | |
| body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--t);font-size:13px;display:flex;min-height:100vh;height:100vh;overflow:hidden;transition:background .3s,color .3s} | |
| .sb{width:var(--sw);background:var(--s);border-right:1px solid var(--bd);display:flex;flex-direction:column;align-items:center;padding:14px 0;gap:4px;flex-shrink:0} | |
| .sbl{width:34px;height:34px;background:var(--ac);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none} | |
| .sbsep{width:26px;height:1px;background:var(--bd);margin:3px 0} | |
| .sbb{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--t3);font-size:14px;cursor:pointer;transition:all .2s;text-decoration:none;border:none;background:none;position:relative} | |
| .sbb:hover{background:var(--s2);color:var(--t)}.sbb.on{background:var(--acl);color:var(--ac)} | |
| .sbb .tip{position:absolute;left:44px;top:50%;transform:translateY(-50%);background:var(--t);color:var(--bg);font-size:10px;padding:3px 7px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;z-index:300}.sbb:hover .tip{opacity:1} | |
| .sbsp{flex:1}.pw{flex:1;display:flex;flex-direction:column;overflow:hidden} | |
| .topb{min-height:50px;background:var(--s);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:8px 18px;gap:10px;flex-shrink:0} | |
| .topl,.topr{display:flex;align-items:center;gap:9px;min-width:0}.bn{font-size:14px;font-weight:600;white-space:nowrap}.bn b{color:var(--ac)} | |
| .pill{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--gr);background:var(--grb);padding:2px 8px;border-radius:20px} | |
| .ld{width:6px;height:6px;border-radius:50%;background:var(--gr);animation:blink 2s infinite} | |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}} | |
| .clk{font-size:10px;font-family:'DM Mono',monospace;color:var(--t2)} | |
| .lg{display:flex;background:var(--s2);border-radius:5px;padding:2px;border:1px solid var(--bd)} | |
| .lb{font-size:10px;font-weight:500;padding:2px 8px;border:none;background:none;cursor:pointer;border-radius:3px;color:var(--t2);font-family:'DM Sans',sans-serif;transition:all .2s}.lb.on{background:var(--ac);color:#fff} | |
| .ib{width:28px;height:28px;border-radius:5px;border:1px solid var(--bd);background:var(--s);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--t2);font-size:11px;position:relative;transition:all .2s}.ib:hover{background:var(--s2)} | |
| .nd{position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--ac)} | |
| main{flex:1;padding:12px 14px;display:grid;grid-template-columns:minmax(0,1.4fr) minmax(320px,360px);gap:12px;overflow:auto;min-height:0} | |
| .left-col{display:grid;grid-template-rows:auto minmax(230px,1fr) minmax(210px,auto);gap:10px;min-height:0} | |
| .right-col{display:grid;grid-template-rows:minmax(300px,1fr) auto;gap:10px;min-height:0} | |
| .mr{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px} | |
| .mc,.cd{background:var(--s);border:1px solid var(--bd);box-shadow:var(--sh);overflow:hidden} | |
| .mc{border-radius:var(--rs);padding:11px 13px;transition:transform .2s}.mc:hover{transform:translateY(-2px)} | |
| .cd{border-radius:var(--r);padding:12px 14px;display:flex;flex-direction:column}.cd:hover{box-shadow:var(--shlg)} | |
| .mct{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px} | |
| .mcl{font-size:10px;color:var(--t2)}.bx{font-size:9px;font-weight:500;padding:1px 5px;border-radius:4px} | |
| .mv{font-size:20px;font-weight:300;letter-spacing:-.5px;line-height:1}.mu{font-size:10px;color:var(--t2)}.ms{font-size:9px;color:var(--t3);margin-top:2px} | |
| .ct{font-size:11px;font-weight:500;margin-bottom:8px;display:flex;align-items:center;gap:5px;flex-shrink:0}.ct i{color:var(--ac);font-size:10px} | |
| .chart-wrap{flex:1;min-height:210px;position:relative} | |
| .chart-wrap canvas,#pieChart{display:block;width:100%;height:100%} | |
| .stack{display:grid;grid-template-columns:1fr 1fr;gap:10px} | |
| .hmcd{padding:0} | |
| .hmhd{padding:10px 12px 6px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0} | |
| .hmhd .ct{margin-bottom:0} | |
| .hmbd{flex:1;position:relative;min-height:220px} | |
| #hmc2{position:absolute;inset:0;width:100%!important;height:100%!important;display:block} | |
| .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px} | |
| .statbox{background:var(--s2);border:1px solid var(--bd);border-radius:8px;padding:10px 11px} | |
| .statbox .k{font-size:9px;color:var(--t2);text-transform:uppercase;letter-spacing:.04em} | |
| .statbox .v{font-size:18px;font-weight:300;margin-top:4px} | |
| .subline{font-size:10px;color:var(--t2)} | |
| .listbox{display:flex;flex-direction:column;gap:6px} | |
| .vr{display:flex;align-items:center;justify-content:space-between;background:var(--s2);border:1px solid var(--bd);border-radius:8px;padding:8px 10px} | |
| .vrl{display:flex;align-items:center;gap:7px;font-size:11px}.vic{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px}.vp{font-size:13px;font-weight:500} | |
| .al{display:flex;align-items:flex-start;gap:8px;padding:7px 0;border-bottom:1px solid var(--bd)}.al:last-child{border-bottom:none} | |
| .ai{width:22px;height:22px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:9px;flex-shrink:0;margin-top:1px} | |
| .at2{font-size:10px;font-weight:500;margin-bottom:1px}.as2{font-size:9px;color:var(--t2)} | |
| .pill-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}.mini-pill{font-size:9px;padding:3px 7px;border-radius:999px;background:var(--s2);border:1px solid var(--bd);color:var(--t2)} | |
| .empty-note{font-size:10px;color:var(--t3);padding-top:4px} | |
| .chart-fallback{height:100%;min-height:120px;display:flex;align-items:center;justify-content:center;text-align:center;font-size:11px;color:var(--t2);background:var(--s2);border:1px dashed var(--bd);border-radius:8px;padding:10px} | |
| .distribution-body{position:relative;display:flex;align-items:center;gap:10px;min-height:150px} | |
| .pie-box{width:145px;height:145px;flex-shrink:0} | |
| @media(max-width:1180px){ | |
| main{grid-template-columns:1fr} | |
| .left-col,.right-col{grid-template-rows:auto} | |
| .right-col{grid-template-columns:1fr} | |
| .stack{grid-template-columns:1fr} | |
| .mr{grid-template-columns:repeat(2,minmax(0,1fr))} | |
| .chart-wrap{min-height:240px} | |
| .hmbd{min-height:260px} | |
| } | |
| @media(max-width:760px){ | |
| body{height:auto;min-height:100svh;overflow:auto;flex-direction:column} | |
| .sb{position:sticky;top:0;z-index:600;width:100%;height:54px;flex-direction:row;justify-content:center;border-right:none;border-bottom:1px solid var(--bd);padding:8px 10px} | |
| .sbl{margin:0 8px 0 0;width:34px;height:34px} | |
| .sbsep,.sbsp,.sbb .tip{display:none} | |
| .sbb{width:38px;height:38px;flex:0 0 38px} | |
| .pw{overflow:visible;min-width:0} | |
| .topb{align-items:flex-start;flex-direction:column;padding:10px 12px} | |
| .topl,.topr{width:100%;flex-wrap:wrap} | |
| .topr{justify-content:space-between} | |
| main{padding:10px;gap:10px;overflow:visible} | |
| .mr{grid-template-columns:1fr 1fr} | |
| .mc,.cd{border-radius:9px} | |
| .stack{gap:10px} | |
| .chart-wrap{min-height:220px} | |
| .stats-grid{grid-template-columns:1fr 1fr} | |
| .distribution-body{align-items:stretch} | |
| #notifP{top:112px!important;right:10px!important;left:10px!important;width:auto!important} | |
| } | |
| @media(max-width:520px){ | |
| .mr,.stats-grid,#sceneSummaryGrid{grid-template-columns:1fr!important} | |
| .mv{font-size:22px} | |
| .cd{padding:11px 12px} | |
| .chart-wrap{min-height:210px} | |
| .hmbd{min-height:220px} | |
| .distribution-body{flex-direction:column;min-height:0} | |
| .pie-box{width:100%;height:190px} | |
| #pieLegend{width:100%} | |
| .topr .clk{width:100%;order:-1} | |
| .lg{order:1} | |
| .ib{order:2} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="sb"> | |
| <a href="/" class="sbl"><i class="fas fa-satellite-dish"></i></a> | |
| <div class="sbsep"></div> | |
| <a href="/home" class="sbb"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a> | |
| <a href="/dashboard" class="sbb on"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a> | |
| <a href="/logs" class="sbb"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a> | |
| <div class="sbsp"></div> | |
| <button class="sbb" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i><span class="tip">Theme</span></button> | |
| </nav> | |
| <div class="pw"> | |
| <header class="topb"> | |
| <div class="topl"> | |
| <span class="bn">Track<b>IQ</b></span> | |
| <span style="color:var(--bd);font-size:16px;margin:0 1px;">|</span> | |
| <span style="font-size:12px;font-weight:500;" id="t-dash">Dashboard</span> | |
| <div class="pill"><span class="ld"></span> <span id="liveBadge">Live</span></div> | |
| </div> | |
| <div class="topr"> | |
| <span class="clk" id="clk">--:--:--</span> | |
| <div class="lg"> | |
| <button class="lb on" onclick="setLang('en',this)">EN</button> | |
| <button class="lb" onclick="setLang('fr',this)">FR</button> | |
| <button class="lb" onclick="setLang('wo',this)">WO</button> | |
| </div> | |
| <button class="ib" onclick="toggleNotifs()"><i class="fas fa-bell"></i><span class="nd" id="nd"></span></button> | |
| <button class="ib" onclick="toggleTheme()"><i class="fas fa-moon" id="ti2"></i></button> | |
| </div> | |
| </header> | |
| <div id="notifP" style="display:none;position:fixed;top:57px;right:65px;z-index:500;width:255px;background:var(--s);border:1px solid var(--bd);border-radius:var(--r);box-shadow:var(--shlg);padding:12px 14px;"> | |
| <div style="font-size:11px;font-weight:500;margin-bottom:8px;display:flex;justify-content:space-between;"><span id="t-notif">Notifications</span><button onclick="clearN()" style="font-size:10px;color:var(--ac);background:none;border:none;cursor:pointer;">Clear all</button></div> | |
| <div id="nl"> | |
| <div style="font-size:10px;color:var(--t3);text-align:center;padding:10px;">No notifications yet</div> | |
| </div> | |
| </div> | |
| <main> | |
| <section class="left-col"> | |
| <div class="mr"> | |
| <div class="mc"><div class="mct"><span class="mcl" id="t-m1">Objects Today</span><span class="bx" style="background:var(--grb);color:var(--gr);" id="m1Trend">+0%</span></div><div><span class="mv" id="mvToday">0</span> <span class="mu" id="t-veh">obj.</span></div><div class="ms" id="m1Sub">Latest analyzed scenes</div></div> | |
| <div class="mc"><div class="mct"><span class="mcl" id="t-m2">Current Flow</span><span class="bx" style="background:var(--grb);color:var(--gr);" id="flowBadge">Smooth</span></div><div><span class="mv" id="mvFlow">0</span> <span class="mu">obj/min</span></div><div class="ms" id="m2Sub">Based on recent activity</div></div> | |
| <div class="mc"><div class="mct"><span class="mcl" id="t-m3">Avg FPS</span><span class="bx" style="background:var(--blb);color:var(--bl);" id="fpsBadge">Stable</span></div><div><span class="mv" id="mvFps">0</span> <span class="mu">fps</span></div><div class="ms" id="m3Sub">Last completed analysis</div></div> | |
| <div class="mc"><div class="mct"><span class="mcl" id="t-m4">Latest clip</span><span class="bx" style="background:var(--ywb);color:var(--yw);" id="accBadge">Current</span></div><div><span class="mv" id="mvAcc">0</span> <span class="mu">s</span></div><div class="ms" id="m4Sub">Duration of latest processed video</div></div> | |
| </div> | |
| <div class="stack"> | |
| <div class="cd"> | |
| <div class="ct"><i class="fas fa-chart-area"></i><span id="t-c1">Flow & speed</span></div> | |
| <div class="chart-wrap"><canvas id="fc"></canvas></div> | |
| </div> | |
| <div class="cd"> | |
| <div class="ct"><i class="fas fa-chart-bar"></i><span id="t-c2">Classification</span></div> | |
| <div class="chart-wrap"><canvas id="tc"></canvas></div> | |
| </div> | |
| </div> | |
| <div class="cd"> | |
| <div class="ct"><i class="fas fa-layer-group"></i><span id="t-scenes">Scene summary</span></div> | |
| <div class="stats-grid"> | |
| <div class="statbox"><div class="k" id="t-live-source">Source</div><div class="v" id="liveSource">No source</div><div class="subline" id="liveSourceMeta">Ready after analysis</div></div> | |
| <div class="statbox"><div class="k" id="t-live-objects">Objects</div><div class="v" id="liveObjects">0</div><div class="subline" id="liveObjectsMeta">Peak visible count</div></div> | |
| <div class="statbox"><div class="k" id="t-live-fps">FPS</div><div class="v" id="liveFps">0</div><div class="subline" id="liveFpsMeta">Captured stream rate</div></div> | |
| <div class="statbox"><div class="k" id="t-live-duration">Duration</div><div class="v" id="liveDuration">0s</div><div class="subline" id="liveDurationMeta">Latest processed clip</div></div> | |
| </div> | |
| <div class="pill-row" id="liveClassPills"></div> | |
| <div class="empty-note" id="liveNote">Current summary follows the latest processed analysis instead of the old floating video preview.</div> | |
| <div id="sceneSummaryGrid" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:10px;"></div> | |
| </div> | |
| </section> | |
| <aside class="right-col"> | |
| <div class="cd hmcd"> | |
| <div class="hmhd"><div class="ct"><i class="fas fa-map-marker-alt"></i><span id="t-hm">Position heatmap</span></div><span style="font-size:9px;color:var(--t3);" id="t-hms">Object density</span></div> | |
| <div class="hmbd"><canvas id="hmc2"></canvas></div> | |
| </div> | |
| <!-- <div class="stack"> | |
| <div class="cd" style="padding:11px 12px;"> | |
| <div class="ct"> | |
| <i class="fas fa-chart-pie"></i> | |
| <span>Object Type Distribution</span> | |
| </div> | |
| --> | |
| <!-- Pie chart + légende --> | |
| <!-- | |
| <div class="distribution-body"> | |
| <div class="pie-box"> | |
| <canvas id="pieChart"></canvas> | |
| </div>--> | |
| <!-- Légende dynamique --> | |
| <!-- | |
| <div id="pieLegend" style="flex:1;display:flex;flex-direction:column;gap:5px;"> | |
| <div style="font-size:10px;color:var(--t3);text-align:center;">No data yet</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cd" style="padding:11px 12px;"> | |
| <div class="ct"><i class="fas fa-wave-square"></i><span id="t-live-panel">Live stats</span></div> | |
| <div class="stats-grid"> | |
| <div class="statbox"><div class="k" id="t-det">Detected</div><div class="v" id="sbdet">0</div></div> | |
| <div class="statbox"><div class="k" id="t-acc">Latest FPS</div><div class="v" id="sbacc">0</div></div> | |
| </div> | |
| <div class="empty-note" id="livePanelNote">Heatmap and key stats stay visible at all times.</div> | |
| </div>--> | |
| </div> | |
| </aside> | |
| </main> | |
| </div> | |
| <script> | |
| setInterval(()=>{document.getElementById('clk').textContent=new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit'});},1000); | |
| function toggleTheme(){ | |
| const d=document.documentElement,dk=d.getAttribute('data-theme')==='dark'; | |
| d.setAttribute('data-theme',dk?'light':'dark'); | |
| ['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className=dk?'fas fa-moon':'fas fa-sun';}); | |
| rebuildCharts(); | |
| setTimeout(drawHm,50); | |
| rebuildPie(window._lastPieLabels||[], window._lastPieData||[], window._lastPieColors||[]); | |
| updateFlowChart(window._lastFlowScene); | |
| updateClassificationChart(window._lastCounts||{}); | |
| try{localStorage.setItem('tiq_theme',dk?'light':'dark');}catch(e){} | |
| } | |
| const TL={ | |
| en:{dash:'Dashboard',m1:'Objects Today',veh:'obj.',m2:'Current Flow',m3:'Avg FPS',m4:'Latest clip',c1:'Flow from video',c2:'Classification',hm:'Position heatmap',hms:'Detected positions',bk:'Breakdown',ca:'Cars',mo:'Moto',tr:'Trucks',al:'Alerts',notif:'Notifications',scenes:'Scene summary',livesource:'Source',liveobjects:'Objects',livefps:'FPS',liveduration:'Duration',livepanel:'Live stats',det:'Detected',acc:'Latest FPS'}, | |
| fr:{dash:'Tableau de bord',m1:'Objets du jour',veh:'obj.',m2:'Flux actuel',m3:'FPS moy.',m4:'Dernier clip',c1:'Flux vidéo',c2:'Classification',hm:'Carte de densité',hms:'Positions détectées',bk:'Répartition',ca:'Voitures',mo:'Motos',tr:'Camions',al:'Alertes',notif:'Notifications',scenes:'Résumé scène',livesource:'Source',liveobjects:'Objets',livefps:'FPS',liveduration:'Durée',livepanel:'Stats live',det:'Détectés',acc:'FPS récent'}, | |
| wo:{dash:'Dashboard',m1:'Objets tey',veh:'obj.',m2:'Yóbbu',m3:'FPS moy.',m4:'Clip mujj',c1:'Flux video',c2:'Wërsëg',hm:'Kàrt',hms:'Posision yi',bk:'Wërsëg',ca:'Cars',mo:'Moto',tr:'Trucks',al:'Yëgël',notif:'Yëgël yi',scenes:'Melo scène',livesource:'Source',liveobjects:'Objets',livefps:'FPS',liveduration:'Yoon',livepanel:'Stats live',det:'Yi réy',acc:'FPS mujj'} | |
| }; | |
| function setLang(l,btn){ | |
| document.querySelectorAll('.lb').forEach(b=>b.classList.remove('on')); | |
| btn.classList.add('on'); | |
| const t=TL[l]; | |
| const mp={dash:'t-dash',m1:'t-m1',veh:'t-veh',m2:'t-m2',m3:'t-m3',m4:'t-m4',c1:'t-c1',c2:'t-c2',hm:'t-hm',hms:'t-hms',bk:'t-bk',ca:'t-ca',mo:'t-mo',tr:'t-tr',al:'t-al',notif:'t-notif',scenes:'t-scenes',livesource:'t-live-source',liveobjects:'t-live-objects',livefps:'t-live-fps',liveduration:'t-live-duration',livepanel:'t-live-panel',det:'t-det',acc:'t-acc'}; | |
| Object.entries(mp).forEach(([k,id])=>{const el=document.getElementById(id);if(el&&t[k])el.textContent=t[k];}); | |
| try{localStorage.setItem('tiq_lang',l);}catch(e){} | |
| } | |
| let no=false; | |
| function toggleNotifs(){no=!no;document.getElementById('notifP').style.display=no?'block':'none';if(no)document.getElementById('nd').style.display='none';} | |
| function clearN(){document.getElementById('nl').innerHTML='<div style="font-size:10px;color:var(--t3);text-align:center;padding:10px;">No notifications</div>';no=false;document.getElementById('notifP').style.display='none';} | |
| document.addEventListener('click',e=>{if(no&&!e.target.closest('#notifP')&&!e.target.closest('.ib'))toggleNotifs();}); | |
| window.fch=null; | |
| window.tch=null; | |
| window._tiqHeatPoints=[]; | |
| window._lastPieLabels=[]; | |
| window._lastPieData=[]; | |
| window._lastPieColors=[]; | |
| window._lastFlowScene=null; | |
| window._lastCounts={}; | |
| const CLASS_COLORS = { | |
| 'Vehicle':'#3b82f6', | |
| 'Motorcycle':'#10b981', | |
| 'Truck':'#f59e0b', | |
| 'Bus':'#8b5cf6', | |
| 'Human':'#ec4899', | |
| 'Bicycle':'#06b6d4', | |
| 'Traffic light':'#ef4444', | |
| 'Road sign':'#f97316' | |
| }; | |
| function chartReady(){ | |
| return false; | |
| } | |
| function sizeCanvas(canvasId){ | |
| const c=document.getElementById(canvasId); | |
| if(!c) return null; | |
| const b=c.parentElement; | |
| const w=Math.max(1,b?.clientWidth||c.clientWidth||300); | |
| const h=Math.max(1,b?.clientHeight||c.clientHeight||160); | |
| c.style.display='block'; | |
| c.width=w; | |
| c.height=h; | |
| return {c,ctx:c.getContext('2d'),w,h}; | |
| } | |
| function drawNativeMessage(canvasId,message){ | |
| const box=sizeCanvas(canvasId); | |
| if(!box) return; | |
| const {ctx,w,h}=box; | |
| const dk=document.documentElement.getAttribute('data-theme')==='dark'; | |
| ctx.clearRect(0,0,w,h); | |
| ctx.fillStyle=dk?'#151c25':'#f6f8fb'; | |
| ctx.fillRect(0,0,w,h); | |
| ctx.fillStyle=dk?'rgba(255,255,255,0.45)':'rgba(15,25,35,0.45)'; | |
| ctx.font="12px 'DM Sans',sans-serif"; | |
| ctx.textAlign='center'; | |
| ctx.textBaseline='middle'; | |
| ctx.fillText(message,w/2,h/2); | |
| } | |
| function drawNativePie(labels,data,colors){ | |
| const box=sizeCanvas('pieChart'); | |
| if(!box) return; | |
| const {ctx,w,h}=box; | |
| const dk=document.documentElement.getAttribute('data-theme')==='dark'; | |
| const total=data.reduce((a,b)=>a+(Number(b)||0),0); | |
| ctx.clearRect(0,0,w,h); | |
| if(!total){ | |
| ctx.fillStyle=dk?'#1c2531':'#e2e8f0'; | |
| ctx.beginPath(); | |
| ctx.arc(w/2,h/2,Math.min(w,h)*0.38,0,Math.PI*2); | |
| ctx.fill(); | |
| return; | |
| } | |
| let start=-Math.PI/2; | |
| labels.forEach((_,i)=>{ | |
| const value=Number(data[i])||0; | |
| const end=start+(value/total)*Math.PI*2; | |
| ctx.beginPath(); | |
| ctx.moveTo(w/2,h/2); | |
| ctx.arc(w/2,h/2,Math.min(w,h)*0.42,start,end); | |
| ctx.closePath(); | |
| ctx.fillStyle=colors[i]||'#888'; | |
| ctx.fill(); | |
| ctx.strokeStyle=dk?'#151c25':'#ffffff'; | |
| ctx.lineWidth=2; | |
| ctx.stroke(); | |
| start=end; | |
| }); | |
| } | |
| function drawNativeBar(labels,data,colors){ | |
| const box=sizeCanvas('tc'); | |
| if(!box) return; | |
| const {ctx,w,h}=box; | |
| const {grid,tick}=gc(); | |
| const dk=document.documentElement.getAttribute('data-theme')==='dark'; | |
| const values=data.map(v=>Number(v)||0); | |
| const max=Math.max(1,...values); | |
| const pad={l:28,r:8,t:18,b:36}; | |
| const chartW=w-pad.l-pad.r; | |
| const chartH=h-pad.t-pad.b; | |
| ctx.clearRect(0,0,w,h); | |
| // Grilles | |
| ctx.strokeStyle=grid;ctx.lineWidth=1; | |
| ctx.font="9px 'DM Mono',monospace";ctx.textAlign='right'; | |
| for(let i=0;i<=3;i++){ | |
| const y=pad.t+chartH-(chartH*i/3); | |
| ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(w-pad.r,y);ctx.stroke(); | |
| ctx.fillStyle=tick;ctx.fillText(Math.round(max*i/3),pad.l-4,y+3); | |
| } | |
| if(!labels.length){drawNativeMessage('tc','No classification data yet');return;} | |
| const gap=Math.max(6,chartW*0.08/Math.max(labels.length,1)); | |
| const barW=Math.max(8,(chartW-gap*(labels.length+1))/labels.length); | |
| labels.forEach((label,i)=>{ | |
| const color=colors[i]||'#3b82f6'; | |
| const val=values[i]; | |
| const bh=Math.max(2,(val/max)*chartH); | |
| const x=pad.l+gap*(i+1)+barW*i; | |
| const y=pad.t+chartH-bh; | |
| const r=Math.min(7,barW/2.5); | |
| // Ombre douce | |
| ctx.shadowColor=color+'55'; | |
| ctx.shadowBlur=10;ctx.shadowOffsetY=4; | |
| // Dégradé vertical | |
| const bg=ctx.createLinearGradient(x,y,x,y+bh); | |
| bg.addColorStop(0,color); | |
| bg.addColorStop(1,color+'88'); | |
| ctx.fillStyle=bg; | |
| // Coins arrondis en haut seulement | |
| ctx.beginPath(); | |
| ctx.moveTo(x+r,y); | |
| ctx.lineTo(x+barW-r,y); | |
| ctx.quadraticCurveTo(x+barW,y,x+barW,y+r); | |
| ctx.lineTo(x+barW,y+bh); | |
| ctx.lineTo(x,y+bh); | |
| ctx.lineTo(x,y+r); | |
| ctx.quadraticCurveTo(x,y,x+r,y); | |
| ctx.closePath();ctx.fill(); | |
| ctx.shadowBlur=0;ctx.shadowOffsetY=0; | |
| // Valeur au-dessus | |
| ctx.fillStyle=tick;ctx.font="bold 9px 'DM Sans',sans-serif";ctx.textAlign='center'; | |
| ctx.fillText(val,x+barW/2,y-4); | |
| // Label tronqué en bas | |
| ctx.font="9px 'DM Sans',sans-serif";ctx.fillStyle=tick; | |
| const shortLbl=label.length>7?label.slice(0,6)+'…':label; | |
| ctx.fillText(shortLbl,x+barW/2,pad.t+chartH+14); | |
| // Carré couleur sous le label | |
| ctx.fillStyle=color; | |
| ctx.fillRect(x+barW/2-4,pad.t+chartH+18,8,3); | |
| }); | |
| } | |
| function drawNativeFlow(labels,objects,fps){ | |
| const box=sizeCanvas('fc'); | |
| if(!box) return; | |
| const {ctx,w,h}=box; | |
| const {grid,tick}=gc(); | |
| const obj=objects.map(v=>Number(v)||0); | |
| const fp=fps.map(v=>Number(v)||0); | |
| if(labels.length<2){ | |
| drawNativeMessage('fc','Run an analysis to build flow data'); | |
| return; | |
| } | |
| const pad={l:30,r:28,t:16,b:24}; | |
| const chartW=w-pad.l-pad.r; | |
| const chartH=h-pad.t-pad.b; | |
| const objMax=Math.max(1,...obj); | |
| const fpsMax=Math.max(1,...fp); | |
| ctx.clearRect(0,0,w,h); | |
| // Grilles | |
| ctx.strokeStyle=grid; | |
| ctx.lineWidth=1; | |
| for(let i=0;i<=3;i++){ | |
| const y=pad.t+chartH-(chartH*i/3); | |
| ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(w-pad.r,y);ctx.stroke(); | |
| } | |
| // Points de la courbe Objects | |
| const pts=obj.map((value,i)=>({ | |
| x:pad.l+(chartW*i/Math.max(obj.length-1,1)), | |
| y:pad.t+chartH-((value/objMax)*chartH) | |
| })); | |
| // FIX Option A: Vague remplie avec dégradé bleu | |
| // 1. Remplissage dégradé sous la courbe | |
| const grad=ctx.createLinearGradient(0,pad.t,0,pad.t+chartH); | |
| grad.addColorStop(0,'rgba(59,130,246,0.38)'); | |
| grad.addColorStop(0.5,'rgba(59,130,246,0.12)'); | |
| grad.addColorStop(1,'rgba(59,130,246,0.01)'); | |
| ctx.beginPath(); | |
| ctx.moveTo(pts[0].x,pad.t+chartH); | |
| pts.forEach(p=>ctx.lineTo(p.x,p.y)); | |
| ctx.lineTo(pts[pts.length-1].x,pad.t+chartH); | |
| ctx.closePath(); | |
| ctx.fillStyle=grad; | |
| ctx.fill(); | |
| // 2. Courbe Objects bleue lissée (simulation tension avec points de contrôle) | |
| ctx.beginPath(); | |
| ctx.moveTo(pts[0].x,pts[0].y); | |
| for(let i=1;i<pts.length;i++){ | |
| const cpx=(pts[i-1].x+pts[i].x)/2; | |
| ctx.bezierCurveTo(cpx,pts[i-1].y,cpx,pts[i].y,pts[i].x,pts[i].y); | |
| } | |
| ctx.strokeStyle='#3b82f6'; | |
| ctx.lineWidth=2; | |
| ctx.lineJoin='round'; | |
| ctx.stroke(); | |
| // 3. Points sur la courbe | |
| pts.forEach(p=>{ | |
| ctx.beginPath(); | |
| ctx.arc(p.x,p.y,3,0,Math.PI*2); | |
| ctx.fillStyle='#3b82f6'; | |
| ctx.fill(); | |
| ctx.strokeStyle='#fff'; | |
| ctx.lineWidth=1.5; | |
| ctx.stroke(); | |
| }); | |
| // 4. Courbe FPS verte en tirets | |
| const fpsPts=fp.map((value,i)=>({ | |
| x:pad.l+(chartW*i/Math.max(fp.length-1,1)), | |
| y:pad.t+chartH-((value/fpsMax)*chartH) | |
| })); | |
| ctx.beginPath(); | |
| ctx.setLineDash([4,3]); | |
| ctx.moveTo(fpsPts[0].x,fpsPts[0].y); | |
| for(let i=1;i<fpsPts.length;i++){ | |
| const cpx=(fpsPts[i-1].x+fpsPts[i].x)/2; | |
| ctx.bezierCurveTo(cpx,fpsPts[i-1].y,cpx,fpsPts[i].y,fpsPts[i].x,fpsPts[i].y); | |
| } | |
| ctx.strokeStyle='#10b981'; | |
| ctx.lineWidth=1.5; | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| // 5. Légende en haut | |
| ctx.font="9px 'DM Sans',sans-serif"; | |
| ctx.textAlign='left'; | |
| // carré bleu | |
| ctx.fillStyle='#3b82f6'; | |
| ctx.fillRect(pad.l,4,10,3); | |
| ctx.fillStyle=tick; | |
| ctx.fillText('Objects',pad.l+14,10); | |
| // tiret vert | |
| ctx.setLineDash([4,3]); | |
| ctx.beginPath(); | |
| ctx.moveTo(pad.l+60,7);ctx.lineTo(pad.l+70,7); | |
| ctx.strokeStyle='#10b981';ctx.lineWidth=1.5;ctx.stroke(); | |
| ctx.setLineDash([]); | |
| ctx.fillStyle='#10b981'; | |
| ctx.fillText('FPS',pad.l+74,10); | |
| // 6. Labels axe X | |
| ctx.fillStyle=tick; | |
| ctx.textAlign='center'; | |
| ctx.fillText(labels[0],pad.l,h-7); | |
| ctx.fillText(labels[labels.length-1],w-pad.r,h-7); | |
| } | |
| function updateFlowChart(liveScene){ | |
| window._lastFlowScene=liveScene||null; | |
| const tl=liveScene?.timeline||[]; | |
| if(tl.length<=1){ | |
| drawNativeFlow([],[],[]); | |
| return; | |
| } | |
| const step=Math.max(1,Math.floor(tl.length/20)); | |
| const lbl=[],objDat=[]; | |
| for(let i=0;i<tl.length;i+=step){ | |
| const pt=tl[i]; | |
| lbl.push((pt.ts||0).toFixed(0)+'s'); | |
| const cnt=Object.entries(pt).filter(([k])=>!['frame','ts'].includes(k)).reduce((a,[,v])=>a+(Number(v)||0),0); | |
| objDat.push(cnt); | |
| } | |
| const fpsDat=lbl.map(()=>liveScene.fps||0); | |
| if(chartReady()&&window.fch){ | |
| window.fch.data.labels=lbl; | |
| window.fch.data.datasets[0].data=objDat; | |
| window.fch.data.datasets[1].data=fpsDat; | |
| window.fch.update('none'); | |
| }else{ | |
| drawNativeFlow(lbl,objDat,fpsDat); | |
| } | |
| } | |
| function updateClassificationChart(uc){ | |
| window._lastCounts=uc||{}; | |
| const labels=Object.keys(uc||{}); | |
| const values=Object.values(uc||{}); | |
| const colors=['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899','#06b6d4','#f97316']; | |
| if(chartReady()&&window.tch){ | |
| window.tch.data.labels=labels.length?labels:['-']; | |
| window.tch.data.datasets[0].data=values.length?values:[0]; | |
| window.tch.data.datasets[0].backgroundColor=labels.map((_,i)=>colors[i%colors.length]); | |
| window.tch.update('none'); | |
| }else{ | |
| drawNativeBar(labels,values,labels.map((_,i)=>colors[i%colors.length])); | |
| } | |
| } | |
| function showChartFallback(canvasId, message){ | |
| const canvas=document.getElementById(canvasId); | |
| if(!canvas) return; | |
| canvas.style.display='none'; | |
| const parent=canvas.parentElement; | |
| if(!parent || parent.querySelector('.chart-fallback')) return; | |
| const fb=document.createElement('div'); | |
| fb.className='chart-fallback'; | |
| fb.textContent=message; | |
| parent.appendChild(fb); | |
| } | |
| function hideChartFallback(canvasId){ | |
| const canvas=document.getElementById(canvasId); | |
| if(!canvas) return; | |
| canvas.style.display=''; | |
| const parent=canvas.parentElement; | |
| const fb=parent && parent.querySelector('.chart-fallback'); | |
| if(fb) fb.remove(); | |
| } | |
| function gc(){ | |
| const dk=document.documentElement.getAttribute('data-theme')==='dark'; | |
| return{grid:dk?'rgba(255,255,255,0.05)':'rgba(0,0,0,0.06)',tick:dk?'#4a5a6a':'#94a3b8'}; | |
| } | |
| function hexToRgba(hex,alpha){ | |
| const clean=String(hex||'').replace('#',''); | |
| const value=clean.length===3 | |
| ? clean.split('').map(ch=>ch+ch).join('') | |
| : clean.padEnd(6,'0').slice(0,6); | |
| const num=parseInt(value,16); | |
| const r=(num>>16)&255,g=(num>>8)&255,b=num&255; | |
| return `rgba(${r},${g},${b},${alpha})`; | |
| } | |
| /* ── Pie chart — Object Type Distribution ── */ | |
| window.pch = null; | |
| function rebuildPie(labels=[], data=[], colors=[]){ | |
| if(!chartReady()){ | |
| drawNativePie(labels, data, colors); | |
| return; | |
| } | |
| hideChartFallback('pieChart'); | |
| // Enregistre datalabels seulement si dispo | |
| if(window.ChartDataLabels) Chart.register(ChartDataLabels); | |
| if(window.pch) window.pch.destroy(); | |
| const canvas = document.getElementById('pieChart'); | |
| if(!canvas) return; | |
| const dk = document.documentElement.getAttribute('data-theme')==='dark'; | |
| const defaultColors = ['#3b82f6','#10b981','#f59e0b','#8b5cf6','#ec4899','#06b6d4','#ef4444','#f97316']; | |
| window.pch = new Chart(canvas, { | |
| type: 'pie', | |
| data: { | |
| labels: labels.length ? labels : ['No data'], | |
| datasets: [{ | |
| data: data.length ? data : [1], | |
| backgroundColor: colors.length ? colors : [dk?'#1c2531':'#e2e8f0'], | |
| borderColor: dk ? '#151c25' : '#ffffff', | |
| borderWidth: 2, | |
| hoverOffset: 6, | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| callbacks: { | |
| label: ctx => { | |
| const tot = ctx.dataset.data.reduce((a,b)=>a+b,0); | |
| return ` ${ctx.label}: ${ctx.raw} (${Math.round(ctx.raw/tot*100)}%)`; | |
| } | |
| } | |
| }, | |
| /* Affiche les % à l'intérieur de chaque part */ | |
| datalabels: { | |
| color: '#fff', | |
| font: { size: 11, weight: 'bold' }, | |
| formatter: (value, ctx) => { | |
| const tot = ctx.dataset.data.reduce((a,b)=>a+b,0); | |
| const pct = Math.round(value/tot*100); | |
| return pct >= 5 ? pct+'%' : ''; // cache si trop petit | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function updatePie(uc){ | |
| const COLORS = { | |
| 'Vehicle':'#3b82f6','Motorcycle':'#10b981','Truck':'#f59e0b', | |
| 'Bus':'#8b5cf6','Human':'#ec4899','Bicycle':'#06b6d4', | |
| 'Traffic light':'#ef4444','Road sign':'#f97316' | |
| }; | |
| const EMO = { | |
| 'Vehicle':'🚗','Motorcycle':'🏍','Truck':'🚚','Bus':'🚌', | |
| 'Human':'🚶','Bicycle':'🚲','Traffic light':'🚦','Road sign':'🛑' | |
| }; | |
| const labels = Object.keys(uc); | |
| const data = Object.values(uc); | |
| const colors = labels.map(l => COLORS[l] || '#888'); | |
| const total = data.reduce((a,b)=>a+b,0); | |
| window._lastPieLabels = labels; | |
| window._lastPieData = data; | |
| window._lastPieColors = colors; | |
| // Recrée le chart | |
| rebuildPie(labels, data, colors); | |
| // Met à jour le label central | |
| const ptn = document.getElementById('pieTotalNum'); | |
| if(ptn) ptn.textContent = total; | |
| // Légende dynamique | |
| const leg = document.getElementById('pieLegend'); | |
| if(!leg) return; | |
| leg.innerHTML = ''; | |
| if(!labels.length){ | |
| leg.innerHTML = '<div style="font-size:10px;color:var(--t3);text-align:center;">No data yet</div>'; | |
| return; | |
| } | |
| labels.forEach((lbl, i) => { | |
| const pct = Math.round(data[i]/Math.max(total,1)*100); | |
| const row = document.createElement('div'); | |
| row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:6px;'; | |
| row.innerHTML = ` | |
| <div style="display:flex;align-items:center;gap:5px;font-size:11px;overflow:hidden;"> | |
| <span style="width:9px;height:9px;border-radius:2px;background:${colors[i]};flex-shrink:0;display:inline-block;"></span> | |
| <span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${EMO[lbl]||'📦'} ${lbl}</span> | |
| </div> | |
| <span style="font-size:10px;color:var(--t2);flex-shrink:0;">${data[i]}</span> | |
| `; | |
| leg.appendChild(row); | |
| }); | |
| } | |
| function rebuildCharts(){ | |
| if(!chartReady()){ | |
| drawNativeFlow([],[],[]); | |
| drawNativeBar([],[],[]); | |
| return; | |
| } | |
| hideChartFallback('fc'); | |
| hideChartFallback('tc'); | |
| if(window.fch)window.fch.destroy(); | |
| if(window.tch)window.tch.destroy(); | |
| const {grid,tick}=gc(); | |
| /* Flow chart : 2 datasets (Objects + FPS) — compatible avec trackiq-api.js */ | |
| window.fch=new Chart(document.getElementById('fc'),{ | |
| data:{labels:['—'],datasets:[ | |
| {type:'line',label:'Objects',data:[0],borderColor:'#3b82f6',backgroundColor:'rgba(59,130,246,0.08)',borderWidth:1.5,tension:0.45,pointRadius:0,fill:true,yAxisID:'y'}, | |
| {type:'line',label:'FPS',data:[0],borderColor:'#10b981',backgroundColor:'rgba(16,185,129,0.05)',borderWidth:1.5,tension:0.45,pointRadius:0,yAxisID:'y2'} | |
| ]}, | |
| options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},plugins:{legend:{labels:{boxWidth:8,font:{size:9},color:tick}}},scales:{x:{ticks:{font:{size:9},color:tick,maxTicksLimit:8},grid:{color:grid},border:{display:false}},y:{position:'left',beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}},y2:{position:'right',beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{drawOnChartArea:false},border:{display:false}}}} | |
| }); | |
| /* Classification chart : NON stacké, 1 dataset — compatible avec trackiq-api.js */ | |
| window.tch=new Chart(document.getElementById('tc'),{ | |
| type:'bar', | |
| data:{labels:['—'],datasets:[ | |
| {label:'Count',data:[0],backgroundColor:'#3b82f6',borderRadius:3,borderSkipped:false} | |
| ]}, | |
| options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}},y:{beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}}}} | |
| }); | |
| } | |
| function drawHm(){ | |
| const c=document.getElementById('hmc2'),b=c.parentElement; | |
| const w=b.offsetWidth,h=b.offsetHeight; | |
| if(!w||!h)return; | |
| c.width=w;c.height=h; | |
| const ctx=c.getContext('2d'); | |
| const dk=document.documentElement.getAttribute('data-theme')==='dark'; | |
| ctx.fillStyle=dk?'#0c1219':'#e8eef5'; | |
| ctx.fillRect(0,0,w,h); | |
| ctx.strokeStyle=dk?'rgba(255,255,255,0.07)':'rgba(0,0,0,0.07)'; | |
| ctx.lineWidth=1; | |
| [0.25,0.5,0.75].forEach(r=>{ctx.beginPath();ctx.moveTo(0,h*r);ctx.lineTo(w,h*r);ctx.stroke();ctx.beginPath();ctx.moveTo(w*r,0);ctx.lineTo(w*r,h);ctx.stroke();}); | |
| const pts=Array.isArray(window._tiqHeatPoints)?window._tiqHeatPoints:[]; | |
| if(pts.length){ | |
| let mxR=0,mxH=0; | |
| pts.forEach(p=>{if(p.r>mxR)mxR=p.r;if(p.h>mxH)mxH=p.h;}); | |
| mxR=Math.max(mxR,0.01);mxH=Math.max(mxH,0.01); | |
| pts.forEach(p=>{ | |
| const px=(p.x/mxR)*w,py=(p.y/mxH)*h; | |
| const rad=Math.max(10,Math.min(w,h)*0.018); | |
| const color=p.color||CLASS_COLORS[p.cls]||'#dc2626'; | |
| const grd=ctx.createRadialGradient(px,py,0,px,py,rad); | |
| grd.addColorStop(0,hexToRgba(color,0.55)); | |
| grd.addColorStop(0.42,hexToRgba(color,0.22)); | |
| grd.addColorStop(1,'rgba(0,0,0,0)'); | |
| ctx.fillStyle=grd;ctx.fillRect(0,0,w,h); | |
| }); | |
| // Légende en bas — fond semi-transparent pour lisibilité | |
| const classes=[...new Set(pts.map(p=>p.cls).filter(Boolean))].slice(0,8); | |
| const legH=20; | |
| const legY=h-legH; | |
| // Fond de la légende | |
| ctx.fillStyle=dk?'rgba(12,17,25,0.75)':'rgba(255,255,255,0.82)'; | |
| ctx.fillRect(0,legY,w,legH); | |
| // Items de légende | |
| ctx.font="9px 'DM Sans',sans-serif"; | |
| ctx.textAlign='left'; | |
| let lx=10; | |
| classes.forEach(cls=>{ | |
| const color=CLASS_COLORS[cls]||'#888'; | |
| // Carré couleur | |
| ctx.fillStyle=color; | |
| ctx.fillRect(lx,legY+6,8,8); | |
| // Texte | |
| ctx.fillStyle=dk?'rgba(255,255,255,0.9)':'rgba(15,25,35,0.85)'; | |
| ctx.fillText(cls,lx+11,legY+13); | |
| lx+=ctx.measureText(cls).width+24; | |
| }); | |
| ctx.font="9px 'DM Mono',monospace"; | |
| ctx.fillStyle=dk?'rgba(255,255,255,0.3)':'rgba(0,0,0,0.25)'; | |
| ctx.textAlign='left';ctx.fillText(pts.length+' points',8,13); | |
| }else{ | |
| ctx.fillStyle=dk?'rgba(255,255,255,0.18)':'rgba(0,0,0,0.2)'; | |
| ctx.font="12px 'DM Sans',sans-serif";ctx.textAlign='center'; | |
| ctx.fillText('Run an analysis to build the heatmap',w/2,h/2); | |
| ctx.textAlign='left'; | |
| } | |
| ctx.font="10px 'DM Mono',monospace"; | |
| ctx.fillStyle=dk?'rgba(255,255,255,0.24)':'rgba(0,0,0,0.2)'; | |
| ctx.fillText('N',w/2-3,12);ctx.fillText('S',w/2-3,h-5);ctx.fillText('W',5,h/2+3);ctx.fillText('E',w-10,h/2+3); | |
| } | |
| async function loadHeatmap(){ | |
| try{ | |
| const r=await fetch('/api/logs/rows'); | |
| if(!r.ok)return; | |
| const rows=await r.json(); | |
| if(!rows.length)return; | |
| const pts=[];let mxX=0,mxY=0; | |
| rows.slice(0,4000).forEach(r=>{ | |
| if(r.cx>0&&r.cy>0){ | |
| const cls=r.class_name||r.class||'Unknown'; | |
| pts.push({x:r.cx,y:r.cy,cls,color:CLASS_COLORS[cls]||'#888',r:0,h:0}); | |
| if(r.cx>mxX)mxX=r.cx;if(r.cy>mxY)mxY=r.cy; | |
| } | |
| }); | |
| if(!pts.length)return; | |
| pts.forEach(p=>{p.r=mxX;p.h=mxY;}); | |
| window._tiqHeatPoints=pts; | |
| drawHm(); | |
| }catch(e){} | |
| } | |
| async function loadDashboard(){ | |
| try{ | |
| const res = await fetch('/api/dashboard/stats'); | |
| if(!res.ok) return; | |
| const data = await res.json(); | |
| const uc = data.global_unique_counts || {}; | |
| const scenes = data.scenes || []; | |
| const activeJobs = data.active_jobs || []; | |
| const total = Object.values(uc).reduce((a,b)=>a+b,0); | |
| // Metric cards | |
| const mvToday = document.getElementById('mvToday'); | |
| if(mvToday) mvToday.textContent = total || 0; | |
| const lastScene = scenes[scenes.length-1]; | |
| const nl=document.getElementById('nl'); | |
| const nd=document.getElementById('nd'); | |
| if(nl&&lastScene){ | |
| const notifs=[]; | |
| if((lastScene.total||0)>80) | |
| notifs.push({color:'var(--re)',text:`🚨 Congestion: ${lastScene.total} objects — "${lastScene.video_name||'scene'}"`}); | |
| if(scenes.length>(window._lastSceneCount||0)&&window._lastSceneCount!==undefined) | |
| notifs.push({color:'var(--gr)',text:`✅ New scene: ${lastScene.video_name||lastScene.scene_id}`}); | |
| window._lastSceneCount=scenes.length; | |
| if(notifs.length){ | |
| if(nd)nd.style.display='block'; | |
| if(nl.querySelector('[style*="No notifications"]'))nl.innerHTML=''; | |
| notifs.forEach(n=>{ | |
| const d=document.createElement('div'); | |
| d.style.cssText='padding:6px 0;border-bottom:1px solid var(--bd);'; | |
| d.innerHTML=`<div style="font-size:10px;font-weight:500;color:${n.color};">${n.text}</div><div style="font-size:9px;color:var(--t2);">Just now</div>`; | |
| nl.prepend(d); | |
| }); | |
| } | |
| } | |
| const liveScene = activeJobs.find(j=>j.status==='processing') || activeJobs[0] || lastScene; | |
| const mvFlow = document.getElementById('mvFlow'); | |
| if(mvFlow) mvFlow.textContent = liveScene | |
| ? Math.round((liveScene.total||0)/Math.max((liveScene.processed_frames||liveScene.frames||1),1)*60) : 0; | |
| const mvFps = document.getElementById('mvFps'); | |
| if(mvFps) mvFps.textContent = lastScene?.fps ? lastScene.fps.toFixed(1) : 0; | |
| const mvAcc = document.getElementById('mvAcc'); | |
| if(mvAcc) mvAcc.textContent = liveScene?.status==='processing' | |
| ? Math.round(liveScene.progress||0) | |
| : (lastScene?.duration_s ? lastScene.duration_s : 0); | |
| const accBadge = document.getElementById('accBadge'); | |
| if(accBadge) accBadge.textContent = liveScene?.status==='processing' ? 'Progress %' : 'Current'; | |
| const liveBadge = document.getElementById('liveBadge'); | |
| if(liveBadge) liveBadge.textContent = activeJobs.some(j=>j.status==='processing') ? 'Processing' : 'Live'; | |
| // Scene summary | |
| if(liveScene){ | |
| const ls=document.getElementById('liveSource'); | |
| if(ls) ls.textContent=liveScene.video_name||liveScene.scene_id||'Unknown'; | |
| const lsm=document.getElementById('liveSourceMeta'); | |
| if(lsm) lsm.textContent=liveScene.status==='processing' | |
| ? `${Math.round(liveScene.progress||0)}% processing` | |
| : `${liveScene.duration_s||0}s analyzed`; | |
| const lo=document.getElementById('liveObjects'); | |
| if(lo) lo.textContent=liveScene.total||0; | |
| const lf=document.getElementById('liveFps'); | |
| if(lf) lf.textContent=liveScene.fps?Number(liveScene.fps).toFixed(1):'—'; | |
| const ld=document.getElementById('liveDuration'); | |
| if(ld) ld.textContent=liveScene.status==='processing' | |
| ? `${liveScene.processed_frames||0}/${liveScene.frames||'?' } frames` | |
| : (liveScene.duration_s?liveScene.duration_s+'s':'—'); | |
| const pills=document.getElementById('liveClassPills'); | |
| if(pills){ | |
| pills.innerHTML=''; | |
| Object.entries(liveScene.unique_counts||{}).forEach(([cls,cnt])=>{ | |
| const p=document.createElement('span'); | |
| p.className='mini-pill'; | |
| p.textContent=`${cls}: ${cnt}`; | |
| pills.appendChild(p); | |
| }); | |
| } | |
| } | |
| updatePie(uc); | |
| // Live stats: webcam when active, otherwise latest video analysis | |
| let liveStatsSet=false; | |
| try{ | |
| const wc=await fetch('/api/webcam/stats'); | |
| if(wc.ok){ | |
| const w=await wc.json(); | |
| if(w.active){ | |
| const det=document.getElementById('sbdet'); | |
| const acc=document.getElementById('sbacc'); | |
| if(det) det.textContent=w.detections??0; | |
| if(acc) acc.textContent=w.frame?Math.round(w.frame):'—'; | |
| liveStatsSet=true; | |
| } | |
| } | |
| }catch(e){} | |
| if(!liveStatsSet){ | |
| const det=document.getElementById('sbdet'); | |
| const acc=document.getElementById('sbacc'); | |
| if(det) det.textContent=liveScene?.total ?? total ?? 0; | |
| if(acc) acc.textContent=liveScene?.fps ? Number(liveScene.fps).toFixed(1) : '—'; | |
| } | |
| updateFlowChart(liveScene); | |
| updateClassificationChart(uc); | |
| // Heatmap | |
| loadHeatmap(); | |
| }catch(err){ console.warn('[TrackIQ] dashboard:',err); } | |
| } | |
| setTimeout(rebuildCharts,120); | |
| // NE PAS appeler rebuildPie() sans données — elle sera appelée par loadDashboard() | |
| setTimeout(drawHm,180); | |
| setTimeout(loadDashboard,300); | |
| setInterval(loadDashboard, 5000); | |
| setInterval(loadHeatmap,20000); | |
| window.addEventListener('resize',()=>setTimeout(()=>{ | |
| drawHm(); | |
| rebuildPie(window._lastPieLabels||[], window._lastPieData||[], window._lastPieColors||[]); | |
| updateFlowChart(window._lastFlowScene); | |
| updateClassificationChart(window._lastCounts||{}); | |
| },50)); | |
| (function(){ | |
| try{ | |
| const th=localStorage.getItem('tiq_theme'); | |
| if(th){ | |
| document.documentElement.setAttribute('data-theme',th); | |
| if(th==='dark'){['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className='fas fa-sun';});} | |
| } | |
| const lg=localStorage.getItem('tiq_lang'); | |
| if(lg&&lg!=='en'){ | |
| const btn=document.querySelector('.lb[onclick*="\''+lg+'\'"]'); | |
| if(btn)setLang(lg,btn); | |
| } | |
| }catch(e){} | |
| })(); | |
| </script> | |
| <script>window.__TRACKIQ_INLINE_DASHBOARD__=true;</script> | |
| <script src="/static/trackiq-api.js"></script> | |
| </body> | |
| </html> | |