TRAFFIC_ROAD_APP / templates /dashboard.html
danielle2035's picture
Update templates/dashboard.html
63c2b68 verified
Raw
History Blame Contribute Delete
43.9 kB
<!DOCTYPE html>
<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>