Download index.html from dku-infosci301-Autumn2026/TrafficExceptionExplorer-sg688: direct link, hf CLI and curl.
- Browser
- Download file 26.9 kB
-
https://huggingface.co/spaces/dku-infosci301-Autumn2026/TrafficExceptionExplorer-sg688/resolve/main/index.html
- Command line
-
hf download hf://spaces/dku-infosci301-Autumn2026/TrafficExceptionExplorer-sg688/index.html
-
curl -L -o index.html https://huggingface.co/spaces/dku-infosci301-Autumn2026/TrafficExceptionExplorer-sg688/resolve/main/index.html
26.9 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <meta name="description" content="Interactive prototype for locating recurring traffic occupancy exceptions." /> | |
| <title>Traffic Exception Explorer</title> | |
| <style> | |
| :root { --bg:#071521; --panel:#0d2233; --panel2:#112b3e; --line:#284458; --text:#eef6fb; --muted:#9bb1bf; --cyan:#20c6e8; --green:#64c89c; --amber:#ffb524; --orange:#f06c2e; --red:#df4541; --missing:#586875; --radius:14px; } | |
| * { box-sizing:border-box; } body { margin:0; background:radial-gradient(circle at 75% -15%,#17445a 0%,transparent 32%),var(--bg); color:var(--text); font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; font-size:14px; } | |
| button,select,input { font:inherit; } button { cursor:pointer; } a { color:#7bdcf0; } | |
| .app { max-width:1600px; margin:0 auto; padding:24px clamp(16px,3vw,44px) 42px; } | |
| header { display:flex; justify-content:space-between; gap:18px; align-items:flex-start; border-bottom:1px solid var(--line); padding-bottom:18px; margin-bottom:18px; } | |
| .eyebrow { color:var(--cyan); font-size:11px; font-weight:800; letter-spacing:.12em; text-transform:uppercase; margin:0 0 6px; } h1 { font-size:clamp(25px,3vw,37px); line-height:1.08; margin:0; letter-spacing:-.04em; } .subtitle { color:var(--muted); margin:8px 0 0; } | |
| .header-tools { display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end; }.badge { border:1px solid #37728c; color:#a9e7f3; border-radius:999px; padding:7px 10px; font-size:12px; white-space:nowrap; }.source { color:var(--muted); font-size:12px; max-width:290px; text-align:right; } | |
| .source a { display:block; margin-top:4px; overflow-wrap:anywhere; } | |
| .notice { display:flex; gap:12px; align-items:flex-start; margin-bottom:18px; border:1px solid #976f1e; background:#2b2314; padding:12px 14px; border-radius:10px; color:#ffe4a7; font-size:13px; }.notice strong { color:#fff1c9; }.notice .dot { width:9px; height:9px; margin-top:4px; flex:0 0 auto; border-radius:50%; background:var(--amber); } | |
| .insight { display:grid; grid-template-columns:1.45fr repeat(3,.62fr) auto; gap:0; background:linear-gradient(105deg,#092b44,#10243c); border:1px solid #23516c; border-radius:var(--radius); overflow:hidden; margin-bottom:18px; }.insight-main,.metric,.insight-action { padding:18px; }.insight-main { display:flex; align-items:flex-start; gap:13px; }.bulb { display:grid; place-items:center; width:38px; height:38px; border-radius:50%; background:#123a55; color:var(--cyan); border:1px solid #286b8f; flex:0 0 auto; font-size:19px; }.insight-label { color:#b6cbd7; font-size:11px; text-transform:uppercase; font-weight:800; letter-spacing:.08em; }.insight-text { font-size:18px; line-height:1.25; font-weight:750; margin-top:5px; }.metric { border-left:1px solid #2a4a5d; display:flex; flex-direction:column; justify-content:center; }.metric strong { font-size:25px; letter-spacing:-.04em; }.metric span { color:var(--muted); font-size:12px; }.insight-action { display:flex; align-items:center; justify-content:center; }.primary { border:0; border-radius:8px; background:var(--amber); color:#151005; font-weight:800; padding:10px 13px; box-shadow:0 5px 16px #0003; }.primary:hover { background:#ffc64a; } | |
| .workspace { display:grid; grid-template-columns:255px minmax(0,1fr) 285px; gap:16px; }.panel { background:linear-gradient(180deg,var(--panel),#0a1d2c); border:1px solid var(--line); border-radius:var(--radius); }.filter-panel { padding:16px; }.section-title { margin:0 0 5px; font-size:15px; letter-spacing:-.01em; }.section-copy { color:var(--muted); font-size:12px; line-height:1.45; margin:0 0 15px; }.control { margin:13px 0; }.control label { display:flex; justify-content:space-between; color:#c3d3dc; font-size:12px; font-weight:700; margin-bottom:6px; }.control select,.control input[type="search"] { width:100%; border:1px solid #315269; color:#eaf4f8; background:#0a1926; border-radius:7px; padding:9px 10px; outline:none; }.control select:focus,.control input:focus { border-color:var(--cyan); box-shadow:0 0 0 2px #20c6e822; }.range-value { color:var(--cyan); }.range { width:100%; accent-color:var(--cyan); }.toggle-row { display:flex; gap:7px; }.day-button,.quality-button { flex:1; background:#102434; color:#abc0cd; border:1px solid #27475b; border-radius:7px; padding:8px 4px; font-size:11px; font-weight:700; }.day-button.active,.quality-button.active { background:#0ba6c7; color:#05202a; border-color:#0ba6c7; }.quality-grid { display:grid; grid-template-columns:1fr 1fr; gap:7px; }.rule { height:1px; background:var(--line); margin:16px 0; }.text-button { width:100%; color:#c7dae5; background:transparent; border:1px dashed #477087; border-radius:7px; padding:8px; font-weight:700; font-size:12px; }.text-button:hover { background:#143042; }.provenance { margin-top:15px; padding:11px; border:1px solid #28627c; background:#0b2738; border-radius:9px; }.provenance h3 { margin:0 0 5px; font-size:12px; color:#d5f4fa; }.provenance p { margin:0 0 8px; color:#b5cbd7; font-size:11px; line-height:1.35; }.trail { display:flex; flex-wrap:wrap; gap:6px; }.trail button { color:#d8f7fc; background:#103a4b; border:1px solid #2d738c; border-radius:999px; padding:5px 8px; font-size:10px; }.trail button:hover,.trail button:focus-visible { background:#17627a; outline:2px solid #8be9f8; outline-offset:2px; }.trail-empty { color:#9bb1bf; font-size:11px; } | |
| .middle { min-width:0; }.chart-panel { padding:16px; }.chart-header { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; margin-bottom:14px; }.legend { display:flex; gap:7px; align-items:center; color:var(--muted); font-size:11px; white-space:nowrap; }.legend-gradient { width:104px; height:8px; border-radius:999px; background:linear-gradient(90deg,#144f85,#16b6d5,#f0bd3e,#e24b35); }.heatmap-wrap { overflow-x:auto; padding-bottom:4px; }.heatmap { display:grid; grid-template-columns:135px repeat(8,minmax(62px,1fr)); min-width:650px; gap:3px; }.week-label,.row-label { color:#a5bbc7; font-size:11px; font-weight:700; padding:7px 4px; }.week-label { text-align:center; }.row-label { display:flex; align-items:center; }.cell { position:relative; min-height:52px; border:1px solid #24516c; border-radius:5px; color:white; font-weight:800; font-size:13px; background:#174f7f; transition:transform .12s,border-color .12s,box-shadow .12s; }.cell:hover,.cell:focus-visible { outline:none; transform:translateY(-2px); border-color:#c8fbff; box-shadow:0 0 0 2px #20c6e855; z-index:2; }.cell.selected { border:3px solid var(--cyan); }.cell .state-mark { position:absolute; right:4px; top:3px; font-size:10px; color:#e9f3f8; }.cell.missing { background:repeating-linear-gradient(135deg,#3f5160 0 5px,#526674 5px 10px); color:#d8e2e6; }.cell.undated { background:repeating-linear-gradient(45deg,#5a4e5b 0 5px,#6c5c68 5px 10px); }.cell.imputed { background:repeating-linear-gradient(90deg,#38646e 0 5px,#487785 5px 10px); }.heatmap-note { color:var(--muted); font-size:11px; margin:12px 0 0; }.detail-panel { padding:16px; }.selected-name { margin:13px 0 10px; font-size:16px; font-weight:800; }.record-line { display:flex; justify-content:space-between; gap:10px; border-bottom:1px solid #213f51; padding:8px 0; font-size:12px; }.record-line span { color:var(--muted); }.record-line strong.high { color:#ff7655; }.record-line strong.warn { color:#ffca4c; }.reason { background:#352919; border:1px solid #936d1e; border-radius:8px; padding:10px; margin:14px 0; color:#f8d98f; font-size:12px; line-height:1.4; }.status { display:inline-flex; align-items:center; gap:5px; border-radius:999px; padding:4px 7px; font-size:11px; font-weight:800; text-transform:capitalize; }.status.observed { background:#0c665e; color:#a4f4dc; }.status.missing { background:#596875; color:#edf3f6; }.status.undated { background:#6a4f68; color:#ffe1ef; }.status.imputed { background:#3a6a72; color:#c4f7fa; }.raw { margin-top:15px; }.raw h3 { font-size:12px; color:#bfd1db; margin:0 0 7px; }.raw-table { width:100%; border-collapse:collapse; font-size:11px; }.raw-table th { color:#9fb3bf; font-weight:700; text-align:left; }.raw-table td,.raw-table th { border-bottom:1px solid #213f51; padding:6px 0; } | |
| .bottom { display:grid; grid-template-columns:1.3fr .85fr; gap:16px; margin-top:16px; }.trend { padding:16px; }.trend-header { display:flex; justify-content:space-between; gap:10px; align-items:baseline; }.trend-header p { color:var(--muted); margin:2px 0 0; font-size:12px; }.trend-svg { width:100%; height:180px; display:block; margin-top:10px; }.governance { padding:16px; }.governance h2 { font-size:15px; margin:0 0 10px; }.governance ul { padding-left:18px; margin:0; color:#c4d4dc; font-size:12px; line-height:1.55; }.governance li::marker { color:var(--cyan); }.error { display:none; margin:0 0 16px; border:1px solid #ad4844; background:#341d20; border-radius:10px; padding:12px; color:#ffd5d2; }.error.visible { display:flex; justify-content:space-between; gap:10px; }.error button { background:transparent; color:#ffe1dc; border:0; font-size:16px; } | |
| @media (max-width:1100px){ .workspace{grid-template-columns:230px minmax(0,1fr)}.detail-panel{grid-column:1/-1}.insight{grid-template-columns:1.5fr repeat(3,.65fr)}.insight-action{grid-column:1/-1;border-top:1px solid #2a4a5d;padding:12px}.primary{width:100%} } | |
| @media (max-width:720px){ .app{padding:16px}.workspace,.bottom{grid-template-columns:1fr}.filter-panel{order:2}.middle{order:1}.detail-panel{order:3}.insight{grid-template-columns:1fr 1fr}.insight-main{grid-column:1/-1}.metric{border-top:1px solid #2a4a5d;border-left:0}.header-tools{justify-content:flex-start}.source{text-align:left}.source a{max-width:100%}header{display:block}.header-tools{margin-top:12px}.heatmap{grid-template-columns:118px repeat(8,64px)} } | |
| </style> | |
| </head> | |
| <body> | |
| <main class="app"> | |
| <header> | |
| <div><p class="eyebrow">Historical traffic prototype</p><h1>Traffic Exception Explorer</h1><p class="subtitle">Locate recurring congestion patterns, then inspect the record behind them.</p></div> | |
| <div class="header-tools"><span class="badge">Derived Duyu Traffic sample</span><div class="source">Source dataset<a href="https://huggingface.co/datasets/Duyu/Time-Series-Forecasting-Benchmark-Datasets" target="_blank" rel="noreferrer">Duyu / Time-Series-Forecasting-Benchmark-Datasets ↗</a></div></div> | |
| </header> | |
| <section class="notice" aria-label="Data limitation"><span class="dot"></span><div><strong>Data boundary:</strong> This view reads a small derived sample of historical Bay Area freeway occupancy records. It does not represent current Kunshan traffic conditions or establish a cause of congestion.</div></section> | |
| <section class="error" id="errorState"><span><strong>Source unavailable.</strong> The dashboard retained its limitation and source link. Reload or return to a verified local copy; do not infer a zero-traffic result.</span><button aria-label="Dismiss error" id="dismissError">×</button></section> | |
| <section class="insight" aria-label="Key finding"> | |
| <div class="insight-main"><div class="bulb">◉</div><div><div class="insight-label">Key finding from current view</div><div class="insight-text" id="insightText">Loading selected view…</div></div></div> | |
| <div class="metric"><strong id="flaggedCount">–</strong><span>flagged records</span></div><div class="metric"><strong id="segmentCount">–</strong><span>segments affected</span></div><div class="metric"><strong id="peakValue">–</strong><span>peak occupancy</span></div> | |
| <div class="insight-action"><button class="primary" id="jumpException">Review exception</button></div> | |
| </section> | |
| <section class="workspace"> | |
| <aside class="panel filter-panel" aria-label="Traffic filters"> | |
| <h2 class="section-title">Explore the records</h2><p class="section-copy">Change a filter to recalculate the visible exception pattern.</p> | |
| <div class="control"><label for="period">Historical interval</label><select id="period"><option value="all">All displayed hours</option><option value="early">02:00–05:00</option><option value="late">06:00–09:00</option></select></div> | |
| <div class="control"><label>Time block</label><div class="toggle-row"><button class="day-button active" data-day="early">Early</button><button class="day-button" data-day="late">Later</button></div></div> | |
| <div class="control"><label for="segment">Source series</label><select id="segment"><option value="all">All source series</option></select></div> | |
| <div class="control"><label for="search">Search source series</label><input type="search" id="search" placeholder="e.g. Series 244" /></div> | |
| <div class="control"><label for="threshold">Occupancy threshold <span class="range-value" id="thresholdValue">12%</span></label><input class="range" type="range" id="threshold" min="3" max="35" value="12" /></div> | |
| <div class="control"><label>Data quality</label><div class="quality-grid"><button class="quality-button active" data-quality="all">All states</button><button class="quality-button" data-quality="observed">Observed</button><button class="quality-button" data-quality="missing">Missing</button><button class="quality-button" data-quality="undated">Undated</button><button class="quality-button" data-quality="imputed">Imputed</button></div></div> | |
| <section class="provenance" aria-labelledby="provenanceTitle"><h3 id="provenanceTitle">Analysis trail</h3><p>Local session history only. Select a saved state to restore the controls that produced it.</p><div class="trail" id="trail"><span class="trail-empty">Change a control to record a state.</span></div></section> | |
| <div class="rule"></div><button class="text-button" id="sourceError">Simulate source error</button> | |
| </aside> | |
| <section class="middle"> | |
| <section class="panel chart-panel" id="heatmapPanel" aria-labelledby="heatmapTitle"> | |
| <div class="chart-header"><div><h2 class="section-title" id="heatmapTitle">Occupancy by source series and hour</h2><p class="section-copy">Warmer cells are higher occupancy; a border marks the selected record.</p></div><div class="legend"><span>Low</span><span class="legend-gradient"></span><span>High</span></div></div> | |
| <div class="heatmap-wrap"><div class="heatmap" id="heatmap" aria-label="Occupancy heatmap"></div></div><p class="heatmap-note">Each displayed value is observed in the derived sample. Source series identifiers are retained in the detail panel.</p> | |
| </section> | |
| <section class="bottom"> | |
| <section class="panel trend"><div class="trend-header"><div><h2 class="section-title">Selected segment over time</h2><p id="trendSubtitle">Choose a heatmap record.</p></div><span class="status observed" id="trendStatus">observed</span></div><svg class="trend-svg" id="trendSvg" role="img" aria-label="Selected segment occupancy trend"></svg></section> | |
| <aside class="panel governance"><h2>What this view can and cannot show</h2><ul><li>It can show a displayed value relative to a threshold or segment baseline.</li><li>It cannot establish why congestion happened.</li><li>It cannot describe real Kunshan conditions until verified local data and community validation are added.</li></ul></aside> | |
| </section> | |
| </section> | |
| <aside class="panel detail-panel" aria-live="polite"><h2 class="section-title">Selected exception</h2><p class="section-copy">Select a heatmap cell to inspect a record.</p><div class="selected-name" id="selectedName">No record selected</div><div id="detailContent"></div><div class="raw"><h3>Raw displayed record</h3><table class="raw-table"><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody id="rawRows"></tbody></table></div><section class="provenance" aria-labelledby="findingTitle"><h3 id="findingTitle">Finding boundary</h3><p id="findingBoundary">Select a record to show the data and interaction conditions behind this finding.</p></section></aside> | |
| </section> | |
| </main> | |
| <script> | |
| (() => { | |
| let segments = [], weeks = [], data = []; | |
| const state = {period:'all',day:'early',segment:'all',search:'',threshold:12,quality:'all',selected:null,trail:[]}; | |
| const $=s=>document.querySelector(s); const heatmap=$('#heatmap'); | |
| const baselineMode = new URLSearchParams(window.location.search).has('baseline'); | |
| if (baselineMode) document.querySelectorAll('.provenance').forEach(node => node.hidden = true); | |
| const palette = value => value < 7 ? '#144f85' : value < 11 ? '#1989aa' : value < 18 ? '#28b59d' : value < 28 ? '#e68a2c' : '#d9433d'; | |
| function valid(r){return r.status==='observed' && r.occupancy>=state.threshold;} | |
| function viewRows(){return data.filter(r => (state.period==='all'||r.month===state.period) && (state.segment==='all'||r.segment===state.segment) && (!state.search||r.segment.toLowerCase().includes(state.search)) && (state.quality==='all'||r.status===state.quality) && (r.day===state.day));} | |
| function activeSegments(rows){return segments.filter(s=>rows.some(r=>r.segment===s));} | |
| function activeWeeks(rows){return weeks.filter(w=>rows.some(r=>r.week===w.id));} | |
| function renderHeatmap(){const rows=viewRows(), segs=activeSegments(rows), wks=activeWeeks(rows); heatmap.innerHTML=''; heatmap.append(Object.assign(document.createElement('div'),{className:'week-label',textContent:'Road segment'})); wks.forEach(w=>heatmap.append(Object.assign(document.createElement('div'),{className:'week-label',textContent:w.id}))); if(!segs.length){const empty=document.createElement('p');empty.textContent='No records match this filter combination.';empty.style.gridColumn='1/-1';empty.style.color='#9bb1bf';heatmap.append(empty);return;} | |
| segs.forEach(seg=>{heatmap.append(Object.assign(document.createElement('div'),{className:'row-label',textContent:seg}));wks.forEach(w=>{const r=rows.find(x=>x.segment===seg&&x.week===w.id);const b=document.createElement('button');if(!r){b.className='cell missing';b.disabled=true;b.textContent='—';heatmap.append(b);return;}b.className=`cell ${r.status} ${state.selected.id===r.id?'selected':''}`;b.style.background=r.status==='observed'?palette(r.occupancy):'';b.textContent=r.status==='missing'?'—':`${r.occupancy}%`;b.title=`${r.segment}, ${r.week}: ${r.occupancy}% (${r.status})`;b.setAttribute('aria-label',b.title);if(r.status!=='observed')b.innerHTML=`${r.status==='missing'?'—':r.occupancy+'%'}<span class="state-mark">${r.status[0].toUpperCase()}</span>`;b.onclick=()=>{state.selected=r;render();};heatmap.append(b);});});} | |
| function renderInsight(){const rows=viewRows(), flagged=rows.filter(valid), affected=new Set(flagged.map(r=>r.segment));const peak=flagged.sort((a,b)=>b.occupancy-a.occupancy)[0];$('#flaggedCount').textContent=flagged.length;$('#segmentCount').textContent=affected.size;$('#peakValue').textContent=peak?peak.occupancy+'%':'—';$('#insightText').textContent=peak?`${peak.segment} reaches ${peak.occupancy}% in ${peak.week}; it is above the selected ${state.threshold}% threshold.`:'No observed record reaches the selected threshold in this view.';} | |
| function baseline(r){const vals=data.filter(x=>x.segment===r.segment&&x.status==='observed').map(x=>x.occupancy).sort((a,b)=>a-b);return vals.length?vals[Math.floor(vals.length/2)]:null;} | |
| function renderDetail(){let r=state.selected;if(!r){$('#selectedName').textContent='No record selected';return;}const base=baseline(r);const deviation=base===null?'Not available':`${r.occupancy-base>=0?'+':''}${r.occupancy-base} pp`;$('#selectedName').textContent=r.segment;$('#detailContent').innerHTML=`<div class="record-line"><span>Historical interval</span><strong>${r.week}</strong></div><div class="record-line"><span>Occupancy</span><strong class="${r.occupancy>=state.threshold?'high':''}">${r.occupancy}%</strong></div><div class="record-line"><span>Segment baseline</span><strong>${base===null?'—':base+'%'}</strong></div><div class="record-line"><span>Deviation</span><strong class="warn">${deviation}</strong></div><div class="record-line"><span>Data status</span><strong><span class="status ${r.status}">${r.status}</span></strong></div><div class="reason"><strong>Why flagged?</strong><br>${r.status==='observed'?(r.occupancy>=state.threshold?`Observed occupancy is at or above the selected ${state.threshold}% threshold and is ${deviation} from this segment baseline.`:`Observed occupancy is below the selected ${state.threshold}% threshold; it remains selectable for comparison.`):`This record is labelled ${r.status}. It is displayed for transparency and must not be interpreted as a normal observed value.`}</div>`;$('#rawRows').innerHTML=[['record_id',r.recordId],['segment',r.segment],['interval',r.week],['occupancy',r.occupancy+'%'],['quality_status',r.status]].map(([a,b])=>`<tr><td>${a}</td><td>${b}</td></tr>`).join('');$('#trendStatus').className='status '+r.status;$('#trendStatus').textContent=r.status;$('#findingBoundary').textContent=`This finding uses ${r.segment}, ${r.week}, a ${state.threshold}% threshold, ${state.quality==='all'?'all displayed data states':state.quality+' records only'}, and the derived historical Bay Area sample. It is a comparison result, not evidence of a congestion cause or Kunshan condition.`;} | |
| function renderTrend(){const r=state.selected;if(!r)return;const vals=weeks.map(w=>data.find(x=>x.segment===r.segment&&x.week===w.id));const W=520,H=180,P=28,max=40;const points=vals.map((x,i)=>`${P+i*((W-P*2)/(vals.length-1))},${H-P-(x.occupancy/max)*(H-P*2)}`).join(' ');const selectedIndex=vals.findIndex(x=>x.id===r.id);const sx=P+selectedIndex*((W-P*2)/(vals.length-1)),sy=H-P-(r.occupancy/max)*(H-P*2);$('#trendSubtitle').textContent=`${r.segment} · ${r.week} selected`;$('#trendSvg').setAttribute('viewBox',`0 0 ${W} ${H}`);$('#trendSvg').innerHTML=`<line x1="${P}" y1="${H-P}" x2="${W-P}" y2="${H-P}" stroke="#315269"/><line x1="${P}" y1="${P}" x2="${P}" y2="${H-P}" stroke="#315269"/><line x1="${P}" y1="${H-P-(state.threshold/max)*(H-P*2)}" x2="${W-P}" y2="${H-P-(state.threshold/max)*(H-P*2)}" stroke="#ffb524" stroke-dasharray="5 5"/><polyline points="${points}" fill="none" stroke="#20c6e8" stroke-width="3" stroke-linejoin="round"/><circle cx="${sx}" cy="${sy}" r="6" fill="#ffb524" stroke="#fff" stroke-width="2"/><text x="${P}" y="${P-7}" fill="#9bb1bf" font-size="11">${max}%</text><text x="${P}" y="${H-10}" fill="#9bb1bf" font-size="11">0%</text><text x="${W-P-80}" y="${H-P-(state.threshold/max)*(H-P*2)-6}" fill="#ffca5e" font-size="11">threshold ${state.threshold}%</text>`;} | |
| function trailLabel(s){return `${s.segment==='all'?'all series':s.segment} · ${s.day} · ≥${s.threshold}%`;} | |
| function recordState(){const snapshot={period:state.period,day:state.day,segment:state.segment,search:state.search,threshold:state.threshold,quality:state.quality};const last=state.trail[0];if(!last||JSON.stringify(last)!==JSON.stringify(snapshot))state.trail.unshift(snapshot);state.trail=state.trail.slice(0,5);} | |
| function restoreState(snapshot){Object.assign(state,snapshot);$('#period').value=state.period;$('#segment').value=state.segment;$('#search').value=state.search;$('#threshold').value=state.threshold;$('#thresholdValue').textContent=state.threshold+'%';document.querySelectorAll('.day-button').forEach(x=>x.classList.toggle('active',x.dataset.day===state.day));document.querySelectorAll('.quality-button').forEach(x=>x.classList.toggle('active',x.dataset.quality===state.quality));render();} | |
| function renderTrail(){const trail=$('#trail');trail.innerHTML='';if(!state.trail.length){trail.innerHTML='<span class="trail-empty">Change a control to record a state.</span>';return;}state.trail.forEach((snapshot,index)=>{const b=document.createElement('button');b.type='button';b.textContent=`${index===0?'Current: ':'Restore: '}${trailLabel(snapshot)}`;b.setAttribute('aria-label',`Restore analysis state ${trailLabel(snapshot)}`);b.onclick=()=>restoreState(snapshot);trail.append(b);});} | |
| function render(){renderInsight();renderHeatmap();renderDetail();renderTrend();renderTrail();} | |
| async function loadSample(){ | |
| try { | |
| const payload=await fetch('./traffic_sample.json').then(response=>{if(!response.ok)throw new Error('Sample unavailable');return response.json();}); | |
| const timestamps=[...new Set(payload.records.map(record=>record[1]))]; | |
| weeks=timestamps.map((timestamp,index)=>({id:timestamp.slice(11,16),month:index<4?'early':'late'})); | |
| segments=[...new Set(payload.records.map(record=>`Series ${record[0]}`))]; | |
| data=payload.records.map(([series,timestamp,occupancy],index)=>{const week=weeks.find(item=>item.id===timestamp.slice(11,16));return {id:`${series}-${timestamp}`,segment:`Series ${series}`,week:week.id,month:week.month,day:week.month,occupancy,status:'observed',recordId:`Traffic.csv:${series}:${timestamp}`,sourceSeriesId:series,sourceTimestamp:timestamp};}); | |
| state.selected=data[0]; | |
| segments.forEach(s=>{const o=document.createElement('option');o.value=s;o.textContent=s;$('#segment').append(o);}); | |
| if (new URLSearchParams(window.location.search).has('demo')) { | |
| state.threshold=18; | |
| $('#threshold').value=state.threshold; | |
| $('#thresholdValue').textContent=state.threshold+'%'; | |
| recordState(); | |
| } | |
| render(); | |
| } catch(error) { | |
| $('#errorState').classList.add('visible'); | |
| $('#insightText').textContent='The derived sample could not be loaded.'; | |
| } | |
| } | |
| $('#period').onchange=e=>{state.period=e.target.value;recordState();render();};$('#segment').onchange=e=>{state.segment=e.target.value;recordState();render();};$('#search').oninput=e=>{state.search=e.target.value.trim().toLowerCase();recordState();render();};$('#threshold').oninput=e=>{state.threshold=+e.target.value;$('#thresholdValue').textContent=state.threshold+'%';recordState();render();}; | |
| document.querySelectorAll('.day-button').forEach(b=>b.onclick=()=>{state.day=b.dataset.day;document.querySelectorAll('.day-button').forEach(x=>x.classList.toggle('active',x===b));recordState();render();});document.querySelectorAll('.quality-button').forEach(b=>b.onclick=()=>{state.quality=b.dataset.quality;document.querySelectorAll('.quality-button').forEach(x=>x.classList.toggle('active',x===b));recordState();render();}); | |
| $('#jumpException').onclick=()=>{const r=viewRows().filter(valid).sort((a,b)=>b.occupancy-a.occupancy)[0];if(r){state.selected=r;render();$('#heatmapPanel').scrollIntoView({behavior:'smooth',block:'center'});}};$('#sourceError').onclick=()=>$('#errorState').classList.add('visible');$('#dismissError').onclick=()=>$('#errorState').classList.remove('visible');loadSample(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |