using System;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
namespace HC_APTBS.ViewModels
{
///
/// Reusable ViewModel for a single real-time scrolling line chart.
/// Backed by LiveChartsCore with a fixed-width sample window.
///
/// Two modes:
///
/// - Index-axis (default): of doubles; X = array index;
/// axis is static. Samples shuffle through fixed X slots on each .
/// - Smooth-scroll (opt-in): of
/// with X on a monotonic sample-index timeline. A host-driven per-frame hook calls
/// to slide the X-axis window, producing true continuous
/// leftward motion independent of data cadence.
///
///
public sealed partial class SingleFlowChartViewModel : ObservableObject
{
private const int DefaultMaxSamples = 200;
private const int SmoothTrimMargin = 4;
private readonly bool _smoothScroll;
private readonly int _maxSamples;
// Index-axis mode storage.
private readonly ObservableCollection? _values;
// Smooth-scroll mode storage + timeline state.
// The series holds N committed points at integer X positions plus one trailing
// "endpoint" whose X/Y interpolate each render frame from the previous committed
// point toward the pending sample. On each new sample the endpoint snaps to its
// target (becoming committed) and a new endpoint is appended at the same spot.
// Net effect: the line tip slides continuously instead of hopping +1 in X per sample.
private readonly ObservableCollection? _points;
private long _nextCommitX;
private double _prevCommittedValue;
private double _pendingValue;
private DateTime _pendingArrivalUtc;
private int _sampleCount;
private ObservablePoint? _endpoint;
/// Chart title label.
[ObservableProperty] private string _title = string.Empty;
/// Most recent sample value, displayed as a numeric label alongside the chart.
[ObservableProperty] private double _currentValue;
/// Series array bound to the CartesianChart.
public ISeries[] Series { get; }
/// X axes for the chart (auto-scrolling, no labels).
public Axis[] XAxes { get; }
/// Y axes for the chart.
public Axis[] YAxes { get; }
///
/// Tolerance band sections overlaid on the chart.
/// Updated when is called.
///
[ObservableProperty] private RectangularSection[] _sections = Array.Empty();
///
/// Creates a new chart ViewModel.
///
/// Display title for the chart.
/// SKColor for the line series.
/// Maximum number of samples before the oldest is dropped.
///
/// When true, store samples on a monotonic X timeline and expect the host to call
/// each render frame to slide the visible window.
/// When false (default), use the legacy index-axis behavior.
///
public SingleFlowChartViewModel(string title, SKColor lineColor, int maxSamples = DefaultMaxSamples, bool smoothScroll = false)
{
_title = title;
_maxSamples = maxSamples;
_smoothScroll = smoothScroll;
if (smoothScroll)
{
_points = new ObservableCollection();
Series = new ISeries[]
{
new LineSeries
{
Values = _points,
Fill = null,
GeometrySize = 0,
Stroke = new SolidColorPaint(lineColor, 2),
LineSmoothness = 0,
AnimationsSpeed = TimeSpan.Zero
}
};
XAxes = new Axis[]
{
new Axis
{
IsVisible = false,
AnimationsSpeed = TimeSpan.Zero,
MinLimit = 0,
MaxLimit = maxSamples
}
};
}
else
{
_values = new ObservableCollection();
Series = new ISeries[]
{
new LineSeries
{
Values = _values,
Fill = null,
GeometrySize = 0,
Stroke = new SolidColorPaint(lineColor, 2),
LineSmoothness = 0,
AnimationsSpeed = TimeSpan.Zero
}
};
XAxes = new Axis[]
{
new Axis
{
IsVisible = false,
AnimationsSpeed = TimeSpan.Zero
}
};
}
YAxes = new Axis[]
{
new Axis
{
AnimationsSpeed = TimeSpan.Zero,
MinLimit = 0,
SeparatorsPaint = new SolidColorPaint(new SKColor(224, 224, 224), 0.75f)
}
};
}
///
/// Appends a value to the chart. Drops the oldest value when the window is full.
/// Must be called on the UI thread.
///
public void AddValue(double value)
{
CurrentValue = value;
if (_smoothScroll)
{
_sampleCount++;
if (_sampleCount == 1)
{
// First sample — commit at X=0 and anchor the viewport so the point sits
// at the right edge. No endpoint yet; we need two samples before we can
// interpolate between previous and pending.
_points!.Add(new ObservablePoint(0, value));
_nextCommitX = 1;
_prevCommittedValue = value;
_pendingValue = value;
_pendingArrivalUtc = DateTime.UtcNow;
XAxes[0].MaxLimit = 0;
XAxes[0].MinLimit = -_maxSamples;
}
else if (_sampleCount == 2)
{
// Second sample — create the sliding endpoint at the previous committed
// point. UpdateViewport will interpolate it toward the pending target.
_pendingValue = value;
_pendingArrivalUtc = DateTime.UtcNow;
_endpoint = new ObservablePoint(_nextCommitX - 1, _prevCommittedValue);
_points!.Add(_endpoint);
}
else
{
// Third+ sample — finalize the existing endpoint at its target (making
// it a committed point), advance the commit index, then append a fresh
// endpoint at the just-finalized position. Because the endpoint is
// already at X = _nextCommitX after the last render frame with
// fraction≈1, the viewport continues from the same position without
// a visible hop.
_endpoint!.X = _nextCommitX;
_endpoint.Y = _pendingValue;
_prevCommittedValue = _pendingValue;
_nextCommitX++;
_pendingValue = value;
_pendingArrivalUtc = DateTime.UtcNow;
_endpoint = new ObservablePoint(_nextCommitX - 1, _prevCommittedValue);
_points!.Add(_endpoint);
// Keep a small margin past the visible window so the leftmost point
// doesn't pop as the viewport advances past the trim boundary.
while (_points.Count > _maxSamples + SmoothTrimMargin)
_points.RemoveAt(0);
}
}
else
{
_values!.Add(value);
if (_values.Count > _maxSamples)
_values.RemoveAt(0);
}
}
///
/// Smooth-scroll mode only: slides the X-axis viewport so the rightmost visible edge
/// drifts continuously past the most recent sample, producing frame-rate smooth motion
/// independent of data cadence. Host calls this once per render frame (e.g. from
/// CompositionTarget.Rendering).
///
/// Current time; pass .
///
/// Expected inter-sample period in milliseconds (e.g. 1000/Hz). Used to interpolate
/// fractional progress between samples.
///
public void UpdateViewport(DateTime nowUtc, double nominalPeriodMs)
{
if (!_smoothScroll || _endpoint == null || nominalPeriodMs <= 0)
return;
double fraction = (nowUtc - _pendingArrivalUtc).TotalMilliseconds / nominalPeriodMs;
if (fraction < 0) fraction = 0;
else if (fraction > 1) fraction = 1;
double slideX = (_nextCommitX - 1) + fraction;
double slideY = _prevCommittedValue + (_pendingValue - _prevCommittedValue) * fraction;
_endpoint.X = slideX;
_endpoint.Y = slideY;
XAxes[0].MaxLimit = slideX;
XAxes[0].MinLimit = slideX - _maxSamples;
}
///
/// Sets the tolerance band (displayed as a shaded region on the chart).
///
/// Center value of the tolerance band.
/// Half-width of the tolerance band.
public void SetTolerance(double target, double tolerance)
{
Sections = new[]
{
new RectangularSection
{
Yi = target - tolerance,
Yj = target + tolerance,
Fill = new SolidColorPaint(SKColors.LimeGreen.WithAlpha(40))
}
};
}
///
/// Clears all sample data and tolerance bands. Resets the smooth-scroll timeline.
///
public void Clear()
{
if (_smoothScroll)
{
_points!.Clear();
_nextCommitX = 0;
_prevCommittedValue = 0;
_pendingValue = 0;
_sampleCount = 0;
_endpoint = null;
XAxes[0].MinLimit = 0;
XAxes[0].MaxLimit = _maxSamples;
}
else
{
_values!.Clear();
}
Sections = Array.Empty();
}
}
}