using System; using System.Collections.Generic; namespace XericUI.XChart { /// 输入值不可用于绘图或统计时的处理方式。 public enum XChartInvalidSamplePolicy { Reject, StoreGap, ClampToLastFinite, } /// 按时间排列的单个时序样本。无效样本是折线断开标记,不参与数值统计。 public struct XChartTimeSample { public readonly double Time; public readonly double Value; public readonly bool IsValid; public XChartTimeSample(double time, double value, bool isValid = true) { Time = time; Value = value; IsValid = isValid; } } [Flags] public enum XChartStatisticMask { None = 0, First = 1 << 0, Last = 1 << 1, Minimum = 1 << 2, Maximum = 1 << 3, Mean = 1 << 4, Sum = 1 << 5, Rms = 1 << 6, Count = 1 << 7, Default = First | Last | Minimum | Maximum | Mean | Count, } /// /// 一个屏幕列/时间桶的统计快照。绘制包络时使用 Minimum/Maximum;普通折线可选择 Mean 或 Last。 /// BucketIndex 对应请求窗口的 [0, BucketCount) 横向列;HadInvalid 用于在此处断线。 /// public struct XChartTimeBucket { public int BucketIndex; public int BucketCount; public int ValidCount; public int InvalidCount; public double StartTime; public double EndTime; public double First; public double Last; public double Minimum; public double Maximum; public double Sum; public double Mean; public double Rms; public bool HasValue => ValidCount > 0; public bool HadInvalid => InvalidCount > 0; public double NormalizedCenter => (BucketIndex + 0.5d) / BucketCount; internal void Add(in XChartTimeSample sample) { if (!sample.IsValid) { InvalidCount++; return; } if (ValidCount == 0) { First = Minimum = Maximum = sample.Value; } Last = sample.Value; Minimum = Math.Min(Minimum, sample.Value); Maximum = Math.Max(Maximum, sample.Value); Sum += sample.Value; Rms += sample.Value * sample.Value; ValidCount++; } internal void FinalizeStatistics() { if (ValidCount == 0) { First = Last = Minimum = Maximum = Sum = Mean = Rms = double.NaN; return; } Mean = Sum / ValidCount; Rms = Math.Sqrt(Rms / ValidCount); } } /// /// 面向实时图表的固定容量时序存储。 /// /// 写入达到容量后覆盖最早数据;读取按显示窗口映射到固定数量的像素桶,并仅在数据版本或查询参数改变时重算。 /// 它不持有 Unity 对象,可被后台采集线程调用;返回的数据被复制到调用方列表,避免暴露环形数组。 /// public sealed class XChartTimeSeriesData { private readonly object m_Gate = new object(); private readonly XChartTimeSample[] m_Ring; private readonly XChartTimeBucket[] m_WorkBuckets; private readonly List m_CachedBuckets; private readonly int[] m_TouchedBucketIndices; private int m_Head; private int m_Count; private long m_Version; private bool m_HasLastFinite; private double m_LastFiniteValue; private long m_CachedVersion = -1; private double m_CachedStart; private double m_CachedEnd; private int m_CachedBucketCount; public XChartTimeSeriesData(int capacity, int maximumBucketCount = 2048, XChartInvalidSamplePolicy invalidSamplePolicy = XChartInvalidSamplePolicy.StoreGap) { if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); if (maximumBucketCount <= 0) throw new ArgumentOutOfRangeException(nameof(maximumBucketCount)); Capacity = capacity; MaximumBucketCount = maximumBucketCount; InvalidSamplePolicy = invalidSamplePolicy; m_Ring = new XChartTimeSample[capacity]; m_WorkBuckets = new XChartTimeBucket[maximumBucketCount]; m_TouchedBucketIndices = new int[maximumBucketCount]; m_CachedBuckets = new List(maximumBucketCount); } public int Capacity { get; } public int MaximumBucketCount { get; } public XChartInvalidSamplePolicy InvalidSamplePolicy { get; set; } public int Count { get { lock (m_Gate) return m_Count; } } public long Version { get { lock (m_Gate) return m_Version; } } private long m_DroppedSampleCount; public long DroppedSampleCount { get { lock (m_Gate) return m_DroppedSampleCount; } } /// 写入一条样本。时间必须为有限数;非递增时间会被拒绝,保证窗口映射可预测。 public bool TryAdd(double time, double value) { lock (m_Gate) { if (!IsFinite(time)) { m_DroppedSampleCount++; return false; } if (m_Count > 0 && time <= GetLogicalSample(m_Count - 1).Time) { m_DroppedSampleCount++; return false; } bool valid = IsFinite(value); if (!valid && InvalidSamplePolicy == XChartInvalidSamplePolicy.Reject) { m_DroppedSampleCount++; return false; } if (!valid && InvalidSamplePolicy == XChartInvalidSamplePolicy.ClampToLastFinite && m_HasLastFinite) { value = m_LastFiniteValue; valid = true; } AddUnchecked(new XChartTimeSample(time, valid ? value : double.NaN, valid)); if (valid) { m_LastFiniteValue = value; m_HasLastFinite = true; } m_Version++; return true; } } /// 清除样本和缓存,不改变已配置容量。 public void Clear() { lock (m_Gate) { m_Head = m_Count = 0; m_HasLastFinite = false; m_CachedVersion = -1; m_CachedBuckets.Clear(); m_Version++; } } /// /// 返回 [startTime, endTime] 内的非空统计桶。bucketCount 通常设为绘图区实际像素宽度,超过上限会抛出异常以杜绝隐式扩容。 /// 调用方应复用 output;方法不会分配新的托管数组。 /// public void GetBuckets(double startTime, double endTime, int bucketCount, List output) { if (output == null) throw new ArgumentNullException(nameof(output)); if (!IsFinite(startTime) || !IsFinite(endTime) || endTime <= startTime) throw new ArgumentException("显示窗口必须为有限且递增的时间区间。"); if (bucketCount <= 0 || bucketCount > MaximumBucketCount) throw new ArgumentOutOfRangeException(nameof(bucketCount)); lock (m_Gate) { if (!IsCacheValid(startTime, endTime, bucketCount)) RebuildBuckets(startTime, endTime, bucketCount); output.Clear(); for (int i = 0; i < m_CachedBuckets.Count; i++) output.Add(m_CachedBuckets[i]); } } private bool IsCacheValid(double startTime, double endTime, int bucketCount) { return m_CachedVersion == m_Version && m_CachedStart == startTime && m_CachedEnd == endTime && m_CachedBucketCount == bucketCount; } private void RebuildBuckets(double startTime, double endTime, int bucketCount) { m_CachedBuckets.Clear(); int touchedCount = 0; double duration = endTime - startTime; for (int logicalIndex = 0; logicalIndex < m_Count; logicalIndex++) { XChartTimeSample sample = GetLogicalSample(logicalIndex); if (sample.Time < startTime || sample.Time > endTime) continue; int index = sample.Time == endTime ? bucketCount - 1 : Math.Min(bucketCount - 1, (int)((sample.Time - startTime) / duration * bucketCount)); ref XChartTimeBucket bucket = ref m_WorkBuckets[index]; if (bucket.ValidCount == 0 && bucket.InvalidCount == 0) { bucket.BucketIndex = index; bucket.BucketCount = bucketCount; bucket.StartTime = startTime + duration * index / bucketCount; bucket.EndTime = startTime + duration * (index + 1) / bucketCount; m_TouchedBucketIndices[touchedCount++] = index; } bucket.Add(sample); } Array.Sort(m_TouchedBucketIndices, 0, touchedCount); for (int i = 0; i < touchedCount; i++) { int index = m_TouchedBucketIndices[i]; m_WorkBuckets[index].FinalizeStatistics(); m_CachedBuckets.Add(m_WorkBuckets[index]); m_WorkBuckets[index] = default(XChartTimeBucket); } m_CachedStart = startTime; m_CachedEnd = endTime; m_CachedBucketCount = bucketCount; m_CachedVersion = m_Version; } private void AddUnchecked(in XChartTimeSample sample) { int writeIndex = (m_Head + m_Count) % Capacity; if (m_Count == Capacity) { m_Ring[m_Head] = sample; m_Head = (m_Head + 1) % Capacity; } else { m_Ring[writeIndex] = sample; m_Count++; } } private XChartTimeSample GetLogicalSample(int logicalIndex) { return m_Ring[(m_Head + logicalIndex) % Capacity]; } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } } }