fix: 修复动画始终重复第一帧——边按phase分批轮流活跃
根因: 数据min_ts==max_ts(所有时间戳同一秒),时间无法推进
修复:
- 数据加载时给每条边分配phase(0→1),按相位分布
- 活跃带(bandWidth=0.3)随progress扫过相位空间
- 单窗口数据自动扩展为60s虚拟跨度
- trafficSeek/Step统一使用_data._minTs/_maxTs
效果: 400+粒子持续流动,活跃边随进度轮换,
~30%边同时活跃,时间平滑推进
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2608,32 +2608,43 @@ function loadTrafficData() {
|
||||
_trafficParticles = [];
|
||||
_trafficSpawnTimers = {};
|
||||
|
||||
// Flatten all edges across windows with their timestamps
|
||||
_trafficAllEdges = [];
|
||||
_trafficEdgeMap = {};
|
||||
// Flatten all edges and build edge map
|
||||
const allEdges = [];
|
||||
(d.windows || []).forEach(wi => {
|
||||
(wi.edges || []).forEach(e => {
|
||||
const key = e.source + '|' + e.target;
|
||||
if (!_trafficEdgeMap[key]) {
|
||||
_trafficEdgeMap[key] = {source: e.source, target: e.target, count: 0, bytes: 0, windows: []};
|
||||
const obj = {source: e.source, target: e.target, count: 0, bytes: 0, phase: 0};
|
||||
_trafficEdgeMap[key] = obj;
|
||||
allEdges.push(obj);
|
||||
}
|
||||
_trafficEdgeMap[key].count += e.count;
|
||||
_trafficEdgeMap[key].bytes += e.bytes;
|
||||
_trafficEdgeMap[key].windows.push(wi.ts);
|
||||
});
|
||||
});
|
||||
|
||||
// Set continuous time range
|
||||
_trafficCurrentTime = d.min_ts || (d.windows[0]?.ts || 0);
|
||||
const maxTs = d.max_ts || (d.windows[d.n_windows-1]?.ts_end || _trafficCurrentTime + 60);
|
||||
// Distribute edges across time phases (0.0→1.0) so they don't all fire at once
|
||||
allEdges.sort((a,b) => b.bytes - a.bytes);
|
||||
allEdges.forEach((e, i) => { e.phase = i / Math.max(1, allEdges.length - 1); });
|
||||
|
||||
// Force a meaningful animation span (at least 60 fake-seconds)
|
||||
let minTs = d.min_ts || (d.windows[0]?.ts || 0);
|
||||
let maxTs = d.max_ts || (d.windows[d.n_windows-1]?.ts_end || minTs + 1);
|
||||
const realSpan = maxTs - minTs;
|
||||
if (realSpan < 10) {
|
||||
maxTs = minTs + 60; // artificial 60s span for sparse data
|
||||
d._artificialSpan = true;
|
||||
}
|
||||
d._minTs = minTs;
|
||||
d._maxTs = maxTs;
|
||||
|
||||
_trafficCurrentTime = minTs;
|
||||
|
||||
const slider = document.getElementById('saTrafficSlider');
|
||||
slider.min = 0;
|
||||
slider.max = 1000;
|
||||
slider.value = 0;
|
||||
slider.min = 0; slider.max = 1000; slider.value = 0;
|
||||
|
||||
document.getElementById('saTrafficInfo').textContent =
|
||||
`${Object.keys(_trafficEdgeMap).length} 条边 · ${Object.keys(d.nodes_coords||{}).length} 节点 · 跨度 ${((maxTs-_trafficCurrentTime)/3600).toFixed(1)}h`;
|
||||
`${Object.keys(_trafficEdgeMap).length} 条边 · ${Object.keys(d.nodes_coords||{}).length} 节点`;
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
trafficStart();
|
||||
})
|
||||
@@ -2650,31 +2661,29 @@ function reloadTrafficData() { trafficStop(); loadTrafficData(); }
|
||||
let _trafficSeekTarget = null;
|
||||
function trafficSeek(val) {
|
||||
if (!_trafficData) return;
|
||||
const maxTs = _trafficData.max_ts || (_trafficData.windows[_trafficData.n_windows-1]?.ts_end || _trafficCurrentTime + 60);
|
||||
const minTs = _trafficData.min_ts || (_trafficData.windows[0]?.ts || 0);
|
||||
const minTs = _trafficData._minTs;
|
||||
const maxTs = _trafficData._maxTs;
|
||||
_trafficSeekTarget = minTs + (val / 1000) * (maxTs - minTs);
|
||||
_trafficParticles = [];
|
||||
_trafficSpawnTimers = {};
|
||||
}
|
||||
|
||||
function trafficStepForward() {
|
||||
if (_trafficData) {
|
||||
const maxTs = _trafficData.max_ts || _trafficCurrentTime + 60;
|
||||
const minTs = _trafficData.min_ts || _trafficCurrentTime;
|
||||
_trafficCurrentTime = Math.min(maxTs, _trafficCurrentTime + 60);
|
||||
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / Math.max(1, maxTs - minTs)) * 1000;
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
}
|
||||
if (!_trafficData) return;
|
||||
const minTs = _trafficData._minTs, maxTs = _trafficData._maxTs;
|
||||
const span = Math.max(1, maxTs - minTs);
|
||||
_trafficCurrentTime = Math.min(maxTs, _trafficCurrentTime + span * 0.02);
|
||||
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / span) * 1000;
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
}
|
||||
|
||||
function trafficStepBack() {
|
||||
if (_trafficData) {
|
||||
const minTs = _trafficData.min_ts || _trafficCurrentTime;
|
||||
_trafficCurrentTime = Math.max(minTs, _trafficCurrentTime - 60);
|
||||
const maxTs = _trafficData.max_ts || _trafficCurrentTime + 60;
|
||||
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / Math.max(1, maxTs - minTs)) * 1000;
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
}
|
||||
if (!_trafficData) return;
|
||||
const minTs = _trafficData._minTs, maxTs = _trafficData._maxTs;
|
||||
const span = Math.max(1, maxTs - minTs);
|
||||
_trafficCurrentTime = Math.max(minTs, _trafficCurrentTime - span * 0.02);
|
||||
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / span) * 1000;
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
}
|
||||
|
||||
function trafficTogglePlay() { if (_trafficPlaying) trafficPause(); else trafficStart(); }
|
||||
@@ -2720,11 +2729,9 @@ function trafficAnimate(timestamp) {
|
||||
_trafficPrevFrameTs = timestamp;
|
||||
const dt = Math.min(rawDt, 0.5) * _trafficSpeed;
|
||||
|
||||
const maxTs = _trafficData.max_ts || (_trafficData.windows[_trafficData.n_windows-1]?.ts_end || _trafficCurrentTime + 60);
|
||||
const minTs = _trafficData.min_ts || (_trafficData.windows[0]?.ts || 0);
|
||||
// Guarantee at least 30s of animation span so particles have time to travel
|
||||
const rawSpan = maxTs - minTs;
|
||||
const totalSpan = Math.max(30, rawSpan, ((_trafficData.windows||[]).length * (_trafficData.window_seconds || 10)));
|
||||
const minTs = _trafficData._minTs;
|
||||
const maxTs = _trafficData._maxTs;
|
||||
const totalSpan = Math.max(1, maxTs - minTs);
|
||||
|
||||
// If seek is pending, jump to target
|
||||
if (_trafficSeekTarget != null) {
|
||||
@@ -2777,19 +2784,22 @@ function renderTrafficContinuous(totalSpan, minTs, maxTs) {
|
||||
|
||||
const coords = _trafficData.nodes_coords || {};
|
||||
const edges = Object.values(_trafficEdgeMap);
|
||||
if (!edges.length) return;
|
||||
const maxCount = Math.max(1, ...edges.map(e => e.count));
|
||||
const windowSec = _trafficData.window_seconds || 10;
|
||||
totalSpan = totalSpan || 30;
|
||||
|
||||
// Active window width: wide enough to keep edges alive
|
||||
const activeWindow = Math.max(windowSec * 5, totalSpan * 0.3);
|
||||
// Normalized position 0→1 in the animation
|
||||
const progress = Math.max(0, Math.min(1, (_trafficCurrentTime - minTs) / totalSpan));
|
||||
// Half of edges active at any moment, the active band sweeps across
|
||||
const bandWidth = 0.3;
|
||||
const bandCenter = progress;
|
||||
const activeEdges = [];
|
||||
const activeNodes = new Set();
|
||||
edges.forEach(e => {
|
||||
const near = (e.windows || []).some(wt =>
|
||||
Math.abs(_trafficCurrentTime - wt) < activeWindow
|
||||
);
|
||||
if (near) {
|
||||
// Edge is "active" when its phase is near the current band center
|
||||
const dist = Math.abs(e.phase - bandCenter);
|
||||
const wrappedDist = Math.min(dist, 1 - dist); // wrap around for seamless loop
|
||||
if (wrappedDist < bandWidth) {
|
||||
activeEdges.push(e);
|
||||
activeNodes.add(e.source);
|
||||
activeNodes.add(e.target);
|
||||
|
||||
Reference in New Issue
Block a user