refactor: per-edge continuous lifecycle rendering
后端:
- traffic_timeline_view: 返回edges[{first_ts,last_ts}]替代windows
- datetime.strptime + .timestamp()保留微秒精度
前端:
- 完全移除窗口概念, edge._fs/_ls定义生命周期
- 时间桶索引(O(1)查找可见边)
- 生命周期<5s自动扩展±30s
- 5%软边缘淡入淡出
- 每帧仅检查当前时间桶±1的边(性能优化)
- 最大渲染500条(按count排序)
验证: 500条边, ~127K像素, 206活跃节点, 边持久可见
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2568,13 +2568,15 @@ function generateColors(n) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Traffic Flow Animation (crossfade window transitions) ── */
|
/* ── 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 _trafficAnimId = null;
|
||||||
let _trafficPlaying = false;
|
let _trafficPlaying = false;
|
||||||
let _trafficSpeed = 1;
|
let _trafficSpeed = 1;
|
||||||
let _trafficCurrentTime = 0;
|
let _trafficCurrentTime = 0;
|
||||||
let _trafficPrevFrameTs = 0;
|
let _trafficPrevFrameTs = 0;
|
||||||
let _trafficSeekTarget = null;
|
let _trafficSeekTarget = null;
|
||||||
|
let _trafficEdges = []; // flat array: {source,target,count,bytes,first_ts,last_ts}
|
||||||
|
let _trafficBuckets = {}; // time-bucket → [edge indices]
|
||||||
|
|
||||||
// Canvas overlay
|
// Canvas overlay
|
||||||
let _trafficCanvasId = 'saTrafficCanvas';
|
let _trafficCanvasId = 'saTrafficCanvas';
|
||||||
@@ -2623,53 +2625,46 @@ function loadTrafficData() {
|
|||||||
fetch('/simple/traffic-timeline/', {
|
fetch('/simple/traffic-timeline/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCSRF()},
|
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(r => r.json())
|
||||||
.then(d => {
|
.then(d => {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
if (d.error) { showError(d.error); return; }
|
if (d.error) { showError(d.error); return; }
|
||||||
// Edge positions computed fresh each frame — no cache needed
|
let minTs = d.min_ts || 0;
|
||||||
let ws = d.window_seconds || 10;
|
let maxTs = d.max_ts || (minTs + 60);
|
||||||
let minTs = d.min_ts || (d.windows[0]?.ts || 0);
|
if (maxTs - minTs < 10) maxTs = minTs + 60;
|
||||||
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);
|
|
||||||
|
|
||||||
|
_trafficEdges = d.edges || [];
|
||||||
_trafficData = d;
|
_trafficData = d;
|
||||||
d._minTs = minTs; d._maxTs = maxTs; d._ws = ws;
|
d._minTs = minTs; d._maxTs = maxTs;
|
||||||
_trafficCurrentTime = minTs;
|
|
||||||
|
// 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').min = 0;
|
||||||
document.getElementById('saTrafficSlider').max = 1000;
|
document.getElementById('saTrafficSlider').max = 1000;
|
||||||
document.getElementById('saTrafficSlider').value = 0;
|
document.getElementById('saTrafficSlider').value = 0;
|
||||||
document.getElementById('saTrafficInfo').textContent =
|
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);
|
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||||
trafficStart();
|
trafficStart();
|
||||||
})
|
})
|
||||||
@@ -2678,28 +2673,7 @@ function loadTrafficData() {
|
|||||||
|
|
||||||
function reloadTrafficData() { trafficStop(); loadTrafficData(); }
|
function reloadTrafficData() { trafficStop(); loadTrafficData(); }
|
||||||
|
|
||||||
// ── getWindowMix: find window index + crossfade factor ──
|
// No more windows — edge lifecycle based
|
||||||
// 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};
|
|
||||||
}
|
|
||||||
|
|
||||||
function trafficSeek(val) {
|
function trafficSeek(val) {
|
||||||
if (!_trafficData) return;
|
if (!_trafficData) return;
|
||||||
@@ -2709,14 +2683,12 @@ function trafficSeek(val) {
|
|||||||
|
|
||||||
function trafficStepForward() {
|
function trafficStepForward() {
|
||||||
if (!_trafficData) return;
|
if (!_trafficData) return;
|
||||||
const ws = _trafficData._ws;
|
_trafficSeekTarget = Math.min(_trafficData._maxTs, _trafficCurrentTime + 10);
|
||||||
_trafficSeekTarget = Math.min(_trafficData._maxTs, _trafficCurrentTime + ws);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function trafficStepBack() {
|
function trafficStepBack() {
|
||||||
if (!_trafficData) return;
|
if (!_trafficData) return;
|
||||||
const ws = _trafficData._ws;
|
_trafficSeekTarget = Math.max(_trafficData._minTs, _trafficCurrentTime - 10);
|
||||||
_trafficSeekTarget = Math.max(_trafficData._minTs, _trafficCurrentTime - ws);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function trafficTogglePlay() { if (_trafficPlaying) trafficPause(); else trafficStart(); }
|
function trafficTogglePlay() { if (_trafficPlaying) trafficPause(); else trafficStart(); }
|
||||||
@@ -2732,7 +2704,7 @@ function trafficPause() {
|
|||||||
cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null;
|
cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null;
|
||||||
}
|
}
|
||||||
function trafficStop() {
|
function trafficStop() {
|
||||||
trafficPause(); _trafficData = null;
|
trafficPause(); _trafficData = null; _trafficEdges = []; _trafficBuckets = {};
|
||||||
_removeFlowCanvas();
|
_removeFlowCanvas();
|
||||||
document.getElementById('saTrafficTime').textContent = '--:--:--';
|
document.getElementById('saTrafficTime').textContent = '--:--:--';
|
||||||
document.getElementById('saTrafficInfo').textContent = '';
|
document.getElementById('saTrafficInfo').textContent = '';
|
||||||
@@ -2778,42 +2750,54 @@ function renderTrafficCrossfade() {
|
|||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
ctx.clearRect(0, 0, c.width, c.height);
|
ctx.clearRect(0, 0, c.width, c.height);
|
||||||
|
|
||||||
const wins = _trafficData.windows || [];
|
const edges = _trafficEdges || [];
|
||||||
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);
|
|
||||||
if (!edges.length) return;
|
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();
|
const activeNodes = new Set();
|
||||||
if (w0 && 1 - cf > 0.1) (w0.active_nodes||[]).forEach(n => activeNodes.add(n));
|
let maxCount = 1;
|
||||||
if (w1 && cf > 0.1) (w1.active_nodes||[]).forEach(n => activeNodes.add(n));
|
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 =
|
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];
|
const sc = coords[e.source], dc = coords[e.target];
|
||||||
if (!sc || !dc) return;
|
if (!sc || !dc) return;
|
||||||
const p1 = SA.map.latLngToContainerPoint([sc[0], sc[1]]);
|
const p1 = SA.map.latLngToContainerPoint([sc[0], sc[1]]);
|
||||||
@@ -2822,10 +2806,8 @@ function renderTrafficCrossfade() {
|
|||||||
const midX = (p1.x + p2.x) / 2;
|
const midX = (p1.x + p2.x) / 2;
|
||||||
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.1;
|
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.1;
|
||||||
const ratio = Math.min(1, e.count / maxCount);
|
const ratio = Math.min(1, e.count / maxCount);
|
||||||
const baseAlpha = 0.6 + 0.4 * ratio;
|
const wa = e.alpha * (0.6 + 0.4 * ratio);
|
||||||
const wa = baseAlpha * e.alpha; // window blend
|
|
||||||
|
|
||||||
// Edge trace
|
|
||||||
const grad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
|
const grad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
|
||||||
grad.addColorStop(0, 'rgba(46,196,182,' + wa.toFixed(2) + ')');
|
grad.addColorStop(0, 'rgba(46,196,182,' + wa.toFixed(2) + ')');
|
||||||
grad.addColorStop(1, 'rgba(230,57,70,' + 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.lineWidth = 1.5 + 4 * ratio;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Particles with same window blend
|
|
||||||
const speedMul = 0.5 + 1.5 * ratio;
|
const speedMul = 0.5 + 1.5 * ratio;
|
||||||
const nP = Math.ceil(3 + 6 * ratio);
|
const nP = Math.ceil(3 + 6 * ratio);
|
||||||
for (let pi = 0; pi < nP; pi++) {
|
for (let pi = 0; pi < nP; pi++) {
|
||||||
@@ -2847,7 +2828,7 @@ function renderTrafficCrossfade() {
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2);
|
ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2);
|
||||||
ctx.fillStyle = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.5+0.5*wa) + ')';
|
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.shadowBlur = 5;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
|||||||
@@ -2171,8 +2171,8 @@ def traffic_timeline_view(request):
|
|||||||
if not bj_timestamps:
|
if not bj_timestamps:
|
||||||
return JsonResponse({'error': '无法解析时间戳'}, status=400)
|
return JsonResponse({'error': '无法解析时间戳'}, status=400)
|
||||||
|
|
||||||
min_ts = min(bj_timestamps)
|
min_ts = float(min(bj_timestamps))
|
||||||
max_ts = max(bj_timestamps)
|
max_ts = float(max(bj_timestamps))
|
||||||
|
|
||||||
# Build time windows
|
# Build time windows
|
||||||
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:
|
if n.get('lat') is not None and n.get('lon') is not None:
|
||||||
nodes_coords[n['ip']] = [n['lat'], n['lon']]
|
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({
|
return JsonResponse({
|
||||||
'windows': window_data,
|
'edges': edges_flat,
|
||||||
'window_seconds': window_sec,
|
'window_seconds': window_sec,
|
||||||
'min_ts': min_ts,
|
'min_ts': min_ts,
|
||||||
'max_ts': max_ts,
|
'max_ts': max_ts,
|
||||||
'n_windows': len(window_data),
|
|
||||||
'nodes_coords': nodes_coords,
|
'nodes_coords': nodes_coords,
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user