diff --git a/frontend/src/lib/components/EventTimeline.svelte b/frontend/src/lib/components/EventTimeline.svelte
index 1137318..a4ad78f 100644
--- a/frontend/src/lib/components/EventTimeline.svelte
+++ b/frontend/src/lib/components/EventTimeline.svelte
@@ -103,6 +103,9 @@
let use24Hour = true;
let initialized = false;
let showFilterDropdown = false;
+ let followCurrentTime = false;
+ let previousFollowCurrentTime = false;
+ let isFollowUpdating = false;
// tooltip state (reactive for svelte template)
let tooltipVisible = false;
@@ -266,6 +269,14 @@
if (currentTimeWindow) {
currentTimeWindow.attr('x', xWindowStart).attr('width', windowWidth).attr('opacity', 0.5);
}
+
+ // if following current time, keep the view centered on now
+ // recenter on every tick to move in sync with now indicator
+ if (followCurrentTime && zoom && svg) {
+ isFollowUpdating = true;
+ centerOnTime(nowDate);
+ isFollowUpdating = false;
+ }
} else {
currentTimeIndicator.attr('opacity', 0);
if (currentTimeWindow) {
@@ -274,6 +285,24 @@
}
}
+ function centerOnTime(date) {
+ if (!xScale || !zoom || !svg) return;
+
+ const currentScale = currentTransform ? currentTransform.k : 1;
+
+ const targetX = xScale(date);
+ const centerX = width / 2;
+
+ // calculate the translation needed to center the target date
+ const translateX = centerX - targetX * currentScale;
+
+ // create new transform
+ const newTransform = d3.zoomIdentity.translate(translateX, 0).scale(currentScale);
+
+ // apply transform instantly
+ d3.select(svg).call(zoom.transform, newTransform);
+ }
+
function updateAxesImmediate(scale) {
if (!scale?.domain) return;
@@ -456,15 +485,24 @@
function handleZoom(event) {
if (!xScale) return;
+ // skip all processing if this is a follow mode update
+ if (isFollowUpdating) {
+ currentTransform = event.transform;
+ pendingScale = currentTransform.rescaleX(xScale);
+ return;
+ }
+
currentTransform = event.transform;
pendingScale = currentTransform.rescaleX(xScale);
// schedule RAF update for circle positions
scheduleZoomUpdate();
- // hide time indicator during interaction
- if (currentTimeIndicator) currentTimeIndicator.attr('opacity', 0);
- if (currentTimeWindow) currentTimeWindow.attr('opacity', 0);
+ // hide time indicator during interaction (only if user-initiated)
+ if (event.sourceEvent) {
+ if (currentTimeIndicator) currentTimeIndicator.attr('opacity', 0);
+ if (currentTimeWindow) currentTimeWindow.attr('opacity', 0);
+ }
// debounce expensive operations (axis, virtualization)
updateAxesDebounced(pendingScale);
@@ -473,6 +511,13 @@
function handleZoomEnd() {
if (!pendingScale) return;
+ // skip processing if this is a follow mode update to prevent infinite loop
+ if (isFollowUpdating) {
+ // but still update circle positions so events move with the timeline
+ updateCirclePositionsFast(pendingScale);
+ return;
+ }
+
// on zoom end, do full re-render with virtualization
renderCircles(pendingScale);
updateAxesImmediate(pendingScale);
@@ -826,11 +871,13 @@
// reactive: initialize timeline
$: if (container && svg && !initialized) {
initializeTimeline();
+ }
+
+ // reactive: update interval for now indicator (same rate whether following or not)
+ $: if (initialized && xScale) {
if (currentTimeIntervalId) clearInterval(currentTimeIntervalId);
currentTimeIntervalId = setInterval(() => {
- if (initialized && xScale) {
- updateCurrentTimeIndicatorImmediate();
- }
+ updateCurrentTimeIndicatorImmediate();
}, CURRENT_TIME_UPDATE_MS);
}
@@ -845,6 +892,25 @@
updateAxesImmediate(scale);
}
+ // reactive: follow mode toggled on
+ $: {
+ if (
+ followCurrentTime !== previousFollowCurrentTime &&
+ followCurrentTime &&
+ initialized &&
+ xScale &&
+ zoom &&
+ svg
+ ) {
+ previousFollowCurrentTime = followCurrentTime;
+ // when follow mode is enabled, immediately center on current time
+ const now = new Date();
+ centerOnTime(now);
+ } else if (!followCurrentTime && previousFollowCurrentTime !== followCurrentTime) {
+ previousFollowCurrentTime = followCurrentTime;
+ }
+ }
+
// reactive: filter dropdown click outside
$: if (showFilterDropdown) {
window.addEventListener('click', handleClickOutside);
@@ -1059,6 +1125,12 @@
{/if}
+