feat(tui): interpolate graph series at viewport boundaries

Ensure graph lines extend smoothly to the edges of the visible viewport, preventing abrupt truncations of data series.
This commit is contained in:
Nils Andreas Svee committed 2026-10-06 13:20:35 +07:00
1 parent 1e61f4168d
commit 8e4424764d
1 file changed
+39 -18
+39 -18
View File
@@ -1272,30 +1272,51 @@ fn target_metric_series(
viewport: GraphViewportRange, viewport: GraphViewportRange,
metric: GraphMetric, metric: GraphMetric,
) -> Option<ChartSeries> { ) -> Option<ChartSeries> {
let data = target let points = target.graph_history.iter().filter_map(|sample| {
.graph_history metric
.iter() .value_ns(sample)
.filter(|sample| { .map(|ns| (sample.timestamp.mono, ns as f64 / 1_000_000.0))
sample.timestamp.mono >= viewport.start && sample.timestamp.mono <= viewport.end });
}) let mut data = Vec::new();
.filter_map(|sample| { let mut previous: Option<(Instant, f64)> = None;
metric for point in points {
.value_ns(sample) if let Some(before) = previous {
.map(|ns| (sample_x(sample, viewport), ns as f64 / 1_000_000.0)) // Only genuine crossing segments contribute boundary points. In
}) // particular, never extend the latest sample to the live edge.
.collect::<Vec<_>>(); if before.0 < viewport.start && point.0 > viewport.start {
data.push((0.0, interpolate_graph_value(before, point, viewport.start)));
}
if before.0 < viewport.end && point.0 > viewport.end {
data.push((
viewport.window.as_secs_f64(),
interpolate_graph_value(before, point, viewport.end),
));
}
}
if point.0 > viewport.end {
break;
}
if point.0 >= viewport.start {
data.push((
point.0.duration_since(viewport.start).as_secs_f64(),
point.1,
));
}
previous = Some(point);
}
(!data.is_empty()).then_some(ChartSeries { (!data.is_empty()).then_some(ChartSeries {
style: target_style(target_idx), style: target_style(target_idx),
data, data,
}) })
} }
fn sample_x(sample: &GraphSample, viewport: GraphViewportRange) -> f64 { fn interpolate_graph_value(a: (Instant, f64), b: (Instant, f64), at: Instant) -> f64 {
sample let whole = b.0.duration_since(a.0).as_secs_f64();
.timestamp if whole == 0.0 {
.mono return b.1;
.saturating_duration_since(viewport.start) }
.as_secs_f64() let part = at.duration_since(a.0).as_secs_f64();
a.1 + (b.1 - a.1) * (part / whole)
} }
fn chart_datasets(series: &[ChartSeries]) -> Vec<Dataset<'_>> { fn chart_datasets(series: &[ChartSeries]) -> Vec<Dataset<'_>> {