diff --git a/simple_analysis/templates/simple_analysis/simple_analysis.html b/simple_analysis/templates/simple_analysis/simple_analysis.html index aedbdaa..f9d4070 100644 --- a/simple_analysis/templates/simple_analysis/simple_analysis.html +++ b/simple_analysis/templates/simple_analysis/simple_analysis.html @@ -2568,13 +2568,15 @@ function generateColors(n) { } /* ── Traffic Flow Animation (crossfade window transitions) ── */ -let _trafficData = null; // {windows:[{ts,edges,active_nodes}], nodes_coords, window_seconds, _minTs, _maxTs} +let _trafficData = null; let _trafficAnimId = null; let _trafficPlaying = false; let _trafficSpeed = 1; let _trafficCurrentTime = 0; let _trafficPrevFrameTs = 0; let _trafficSeekTarget = null; +let _trafficEdges = []; // flat array: {source,target,count,bytes,first_ts,last_ts} +let _trafficBuckets = {}; // time-bucket → [edge indices] // Canvas overlay let _trafficCanvasId = 'saTrafficCanvas'; @@ -2623,53 +2625,46 @@ function loadTrafficData() { fetch('/simple/traffic-timeline/', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()}, - body: JSON.stringify({session_id: SA.sessionId, window_seconds: 10, max_edges: 300}), + body: JSON.stringify({session_id: SA.sessionId, window_seconds: 10, max_edges: 500}), }) .then(r => r.json()) .then(d => { hideLoading(); if (d.error) { showError(d.error); return; } - // Edge positions computed fresh each frame — no cache needed - let ws = d.window_seconds || 10; - let minTs = d.min_ts || (d.windows[0]?.ts || 0); - let maxTs = d.max_ts || (d.windows[d.n_windows-1]?.ts_end || minTs + ws); - let wins = d.windows || []; - - // If only 1-2 real windows, split edges into artificial windows for crossfade variety - if (wins.length <= 2 && wins.length > 0) { - const allEdges = []; - const seen = new Set(); - wins.forEach(w => (w.edges||[]).forEach(e => { - const k = e.source + '|' + e.target; - if (!seen.has(k)) { seen.add(k); allEdges.push({...e}); } - })); - allEdges.sort((a,b) => (b.count||0) - (a.count||0)); - // More windows with fewer edges each = more visible change - const nWindows = Math.min(20, Math.ceil(allEdges.length / 20)); - const groupSize = Math.max(15, Math.ceil(allEdges.length / nWindows)); - const newWins = []; - let ti = minTs; - for (let g = 0; g < allEdges.length; g += groupSize) { - const groupEdges = allEdges.slice(g, g + groupSize); - const activeNodes = new Set(); - groupEdges.forEach(e => { activeNodes.add(e.source); activeNodes.add(e.target); }); - newWins.push({ts: ti, t_label: tsToBJ(ti).slice(11,19), edges: groupEdges, active_nodes: [...activeNodes]}); - ti += ws; - } - wins = newWins; - d.windows = wins; - maxTs = ti; - } - if (maxTs - minTs < 10) maxTs = minTs + Math.max(60, ws * 6); + let minTs = d.min_ts || 0; + let maxTs = d.max_ts || (minTs + 60); + if (maxTs - minTs < 10) maxTs = minTs + 60; + _trafficEdges = d.edges || []; _trafficData = d; - d._minTs = minTs; d._maxTs = maxTs; d._ws = ws; - _trafficCurrentTime = minTs; + d._minTs = minTs; d._maxTs = maxTs; + + // Time-bucket index. Stretch short lifetimes so edges are visible. + _trafficBuckets = {}; + const bucketSz = 60; + let minFs = Infinity, maxLs = -Infinity; + _trafficEdges.forEach((e, idx) => { + let fs = e.first_ts, ls = e.last_ts; + if (ls - fs < 5) { fs -= 30; ls += 30; } + e._fs = fs; e._ls = ls; + if (fs < minFs) minFs = fs; + if (ls > maxLs) maxLs = ls; + for (let b = Math.floor(fs / bucketSz); b <= Math.floor(ls / bucketSz); b++) { + if (!_trafficBuckets[b]) _trafficBuckets[b] = []; + _trafficBuckets[b].push(idx); + } + }); + // Use stretched range for animation span + if (minFs < Infinity) d._minTs = minFs; + if (maxLs > -Infinity) d._maxTs = maxLs; + if (d._maxTs - d._minTs < 10) d._maxTs = d._minTs + 60; + _trafficCurrentTime = d._minTs; + document.getElementById('saTrafficSlider').min = 0; document.getElementById('saTrafficSlider').max = 1000; document.getElementById('saTrafficSlider').value = 0; document.getElementById('saTrafficInfo').textContent = - (d.windows||[]).length + ' 窗口 · ' + Object.keys(d.nodes_coords||{}).length + ' 节点 · 跨度' + ((maxTs-minTs)/60).toFixed(0) + 'min'; + _trafficEdges.length + ' 条边 · ' + Object.keys(d.nodes_coords||{}).length + ' 节点 · 连续生命周期'; document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime); trafficStart(); }) @@ -2678,28 +2673,7 @@ function loadTrafficData() { function reloadTrafficData() { trafficStop(); loadTrafficData(); } -// ── getWindowMix: find window index + crossfade factor ── -// crossfade 0→1 over last 20% of window[i], edges from both windows blend -function getWindowMix() { - if (!_trafficData) return {i:0, crossfade:0}; - const wins = _trafficData.windows || []; - if (!wins.length) return {i:0, crossfade:0}; - const ws = _trafficData._ws; - const minTs = _trafficData._minTs; - // Which window does currentTime fall into? - let idx = Math.floor((_trafficCurrentTime - minTs) / ws); - idx = Math.max(0, Math.min(wins.length - 1, idx)); - // How far into this window are we? (0..1) - const wTs = wins[idx].ts; - let progress = 0; - if (ws > 0) progress = Math.max(0, Math.min(1, (_trafficCurrentTime - wTs) / ws)); - // Crossfade: last 20% → blend from window[idx] to window[idx+1] - let crossfade = 0; - if (progress > 0.8) { - crossfade = Math.min(1, (progress - 0.8) / 0.2); - } - return {i: idx, crossfade}; -} +// No more windows — edge lifecycle based function trafficSeek(val) { if (!_trafficData) return; @@ -2709,14 +2683,12 @@ function trafficSeek(val) { function trafficStepForward() { if (!_trafficData) return; - const ws = _trafficData._ws; - _trafficSeekTarget = Math.min(_trafficData._maxTs, _trafficCurrentTime + ws); + _trafficSeekTarget = Math.min(_trafficData._maxTs, _trafficCurrentTime + 10); } function trafficStepBack() { if (!_trafficData) return; - const ws = _trafficData._ws; - _trafficSeekTarget = Math.max(_trafficData._minTs, _trafficCurrentTime - ws); + _trafficSeekTarget = Math.max(_trafficData._minTs, _trafficCurrentTime - 10); } function trafficTogglePlay() { if (_trafficPlaying) trafficPause(); else trafficStart(); } @@ -2732,7 +2704,7 @@ function trafficPause() { cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null; } function trafficStop() { - trafficPause(); _trafficData = null; + trafficPause(); _trafficData = null; _trafficEdges = []; _trafficBuckets = {}; _removeFlowCanvas(); document.getElementById('saTrafficTime').textContent = '--:--:--'; document.getElementById('saTrafficInfo').textContent = ''; @@ -2778,42 +2750,54 @@ function renderTrafficCrossfade() { if (!ctx) return; ctx.clearRect(0, 0, c.width, c.height); - const wins = _trafficData.windows || []; - if (!wins.length) return; - const coords = _trafficData.nodes_coords || {}; - const mix = getWindowMix(); - const cf = mix.crossfade; // 0=only w0, 1=only w1 - const w0 = wins[mix.i] || null; - const w1 = mix.i + 1 < wins.length ? wins[mix.i + 1] : null; - - // Build edge map from both windows with crossfade weights - const edgeMap = {}; - function addEdges(edges, windowAlpha) { - if (!edges || windowAlpha <= 0.02) return; - edges.forEach(e => { - const k = e.source + '|' + e.target; - if (!edgeMap[k]) edgeMap[k] = {source:e.source, target:e.target, alpha:0, count:0}; - // For edges in both windows: keep max alpha (no flicker) - if (windowAlpha > edgeMap[k].alpha) edgeMap[k].alpha = windowAlpha; - if ((e.count||0) > edgeMap[k].count) edgeMap[k].count = e.count||0; - }); - } - addEdges(w0 ? w0.edges : null, 1 - cf); // fading out - addEdges(w1 ? w1.edges : null, cf); // fading in - - const edges = Object.values(edgeMap); + const edges = _trafficEdges || []; if (!edges.length) return; - const maxCount = Math.max(1, ...edges.map(e => e.count)); + const coords = _trafficData.nodes_coords || {}; + const now = _trafficCurrentTime; - // Active nodes: union of both windows + // Use time-bucket index to only check nearby edges + const bucketSz = 60; + const curBucket = Math.floor(now / bucketSz); + let candidateIdx = new Set(); + for (let b = curBucket - 1; b <= curBucket + 1; b++) { + const bucket = _trafficBuckets[b]; + if (bucket) bucket.forEach(idx => candidateIdx.add(idx)); + } + // Fallback: if no buckets (old data), scan all + if (candidateIdx.size === 0) { + edges.forEach((_, i) => candidateIdx.add(i)); + } + + // Filter visible edges + compute alpha + const visible = []; const activeNodes = new Set(); - if (w0 && 1 - cf > 0.1) (w0.active_nodes||[]).forEach(n => activeNodes.add(n)); - if (w1 && cf > 0.1) (w1.active_nodes||[]).forEach(n => activeNodes.add(n)); + let maxCount = 1; + for (const idx of candidateIdx) { + const e = edges[idx]; + const fs = e._fs || e.first_ts, ls = e._ls || e.last_ts; + if (!e || now < fs || now > ls) continue; + const duration = Math.max(1, ls - fs); + const softEdge = Math.max(0.5, duration * 0.05); + let alpha = 1; + if (now - fs < softEdge) alpha = Math.max(0, (now - fs) / Math.max(0.01, softEdge)); + else if (ls - now < softEdge) alpha = Math.max(0, (ls - now) / Math.max(0.01, softEdge)); + if (alpha < 0.03) continue; + visible.push({...e, alpha}); + activeNodes.add(e.source); + activeNodes.add(e.target); + if (e.count > maxCount) maxCount = e.count; + } + + // Limit max rendered edges for performance + if (visible.length > 500) { + visible.sort((a, b) => b.count - a.count); + visible.length = 500; + } document.getElementById('saTrafficInfo').textContent = - edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime); + visible.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(now); - edges.forEach(e => { + visible.forEach(e => { const sc = coords[e.source], dc = coords[e.target]; if (!sc || !dc) return; const p1 = SA.map.latLngToContainerPoint([sc[0], sc[1]]); @@ -2822,10 +2806,8 @@ function renderTrafficCrossfade() { const midX = (p1.x + p2.x) / 2; const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.1; const ratio = Math.min(1, e.count / maxCount); - const baseAlpha = 0.6 + 0.4 * ratio; - const wa = baseAlpha * e.alpha; // window blend + const wa = e.alpha * (0.6 + 0.4 * ratio); - // Edge trace const grad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y); grad.addColorStop(0, 'rgba(46,196,182,' + wa.toFixed(2) + ')'); grad.addColorStop(1, 'rgba(230,57,70,' + wa.toFixed(2) + ')'); @@ -2836,7 +2818,6 @@ function renderTrafficCrossfade() { ctx.lineWidth = 1.5 + 4 * ratio; ctx.stroke(); - // Particles with same window blend const speedMul = 0.5 + 1.5 * ratio; const nP = Math.ceil(3 + 6 * ratio); for (let pi = 0; pi < nP; pi++) { @@ -2847,7 +2828,7 @@ function renderTrafficCrossfade() { ctx.beginPath(); ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2); ctx.fillStyle = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.5+0.5*wa) + ')'; - ctx.shadowColor = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.4+0.4*wa) + ')'; + ctx.shadowColor = 'rgba(' + cr + ',' + cg + ',' + cb + ',0.4)'; ctx.shadowBlur = 5; ctx.fill(); ctx.shadowBlur = 0; diff --git a/simple_analysis/views.py b/simple_analysis/views.py index 251363a..02f7539 100644 --- a/simple_analysis/views.py +++ b/simple_analysis/views.py @@ -2171,8 +2171,8 @@ def traffic_timeline_view(request): if not bj_timestamps: return JsonResponse({'error': '无法解析时间戳'}, status=400) - min_ts = min(bj_timestamps) - max_ts = max(bj_timestamps) + min_ts = float(min(bj_timestamps)) + max_ts = float(max(bj_timestamps)) # Build time windows windows = [] @@ -2246,12 +2246,60 @@ def traffic_timeline_view(request): if n.get('lat') is not None and n.get('lon') is not None: nodes_coords[n['ip']] = [n['lat'], n['lon']] + # ── Per-edge lifecycle: first_ts, last_ts (continuous rendering) ── + # Parse timestamps with microsecond precision using datetime + from datetime import datetime, timezone, timedelta + BJ_TZ = timezone(timedelta(hours=8)) + bj_floats = [] + for t_str in time_list: + try: + t_clean = str(t_str).strip() if t_str else '' + if not t_clean or t_clean in ('None', '', 'nan'): + bj_floats.append(None); continue + # Try common formats + dt = None + for fmt in ['%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%d %H:%M:%S.%f', + '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S']: + try: + dt = datetime.strptime(t_clean[:26], fmt) + break + except ValueError: continue + if dt is None: bj_floats.append(None); continue + # Convert to Beijing time Unix timestamp + bj_floats.append(dt.replace(tzinfo=BJ_TZ).timestamp()) + except Exception: + bj_floats.append(None) + + edge_lifecycle = {} # (src,dst) → {count,bytes,first_ts,last_ts} + for idx in range(len(src_list)): + ts = bj_floats[idx] if idx < len(bj_floats) else None + if ts is None: continue + s = str(src_list[idx]) if idx < len(src_list) and src_list[idx] not in (None, '', 'None') else '?' + d = str(dst_list[idx]) if idx < len(dst_list) and dst_list[idx] not in (None, '', 'None') else '?' + key = (s, d) + if key not in edge_lifecycle: + edge_lifecycle[key] = {'count': 0, 'bytes': 0.0, 'first_ts': ts, 'last_ts': ts} + e = edge_lifecycle[key] + e['count'] += 1 + if bytes_raw and idx < len(bytes_raw): + try: e['bytes'] += float(bytes_raw[idx] or 0) + except Exception: pass + if ts < e['first_ts']: e['first_ts'] = ts + if ts > e['last_ts']: e['last_ts'] = ts + + edges_flat = [] + for (src, dst), e in sorted(edge_lifecycle.items(), key=lambda x: -x[1]['count'])[:max_edges]: + edges_flat.append({ + 'source': src, 'target': dst, + 'count': e['count'], 'bytes': round(e['bytes'], 2), + 'first_ts': e['first_ts'], 'last_ts': e['last_ts'], + }) + return JsonResponse({ - 'windows': window_data, + 'edges': edges_flat, 'window_seconds': window_sec, 'min_ts': min_ts, 'max_ts': max_ts, - 'n_windows': len(window_data), 'nodes_coords': nodes_coords, }) except Exception as e: