绘制实时、不断变化的数据

不断增长的数据是什么样的最好方式?开发者可以选择几个选项。选择最好的,考虑这些要点:

  1. 绘制阵列后,可以继续修改其值。例如,如果将数组加载到信号图中,则可以在绘制后继续修改该数组的值。每当绘图再次渲染时,最新的值将出现在绘图上。这是固定长度数据集的最佳选择,并且始终是线程安全的。

  2. 呼叫Render()在修改数据之后。渲染绘图的成本可能很高,因此在修改数据时不会自动调用绘图。通过调用用户控件的Render()方法对于以高频率更新数据的绘图,明智的做法是使用计时器触发渲染,以达到可接受的帧速率,而不是在每次修改数据时强制进行渲染。

  3. 调整轴限制以适应新数据。如果添加新数据并请求渲染,则新数据可能会从打印区域流出。可能需要在添加新数据时调整轴限制,以确保新数据保留在绘图区域中。AxisAuto()可用于自动将轴限制设置为最新数据的边界,但此方法调用不适用于大型数据集,过于频繁地调用会让希望使用鼠标调整缩放和平移的用户感到不安,不断变化的轴限制可能会让用户感到困惑。考虑实现一些定制,例如检测最新数据点何时运行在数据区域之外,然后仅通过将右轴限制设置为数据加20%的当前跨度来调整轴限制。这将确保屏幕上始终显示最新的数据,并将轴限制的更改次数降至最低。

  4. 在多线程环境中使用RenderLock()以确保在绘图仪处于活动渲染状态时不会修改绘图仪中数据阵列的长度。由于渲染会迭代数组中的每个值,因此在渲染中更改该数组的长度可能会产生数组索引异常。这在单线程GUI应用程序(WinForms)中通常不是一个问题,但在使用多线程(WPF)时可能会出现问题。

更改固定长度数据

绘制阵列后,可以随时更改其值并重新渲染。这是显示不断变化的固定长度数据的最佳选项。虽然这种方法可以用于大多数绘图类型,但信号绘图几乎总是最有效的选择。

在这个例子中,一个固定的长度readonly数组被创建并添加到绘图中,然后Timer调用一个方法来更改该数组中的值并重新渲染绘图。

readonly double[] Values = new double[25];
readonly Stopwatch Stopwatch = Stopwatch.StartNew();

public Form1()
{
    InitializeComponent();
    UpdateValues();
    formsPlot1.Plot.AddSignal(Values);
}

public void UpdateValues()
{
    double phase = Stopwatch.Elapsed.TotalSeconds;
    double multiplier = 2 * Math.PI / Values.Length;
    for (int i = 0; i < Values.Length; i++)
        Values[i] = Math.Sin(i * multiplier + phase);
}

private void timer1_Tick(object sender, EventArgs e)
{
    UpdateValues();
    formsPlot1.Render();
}

滚动定长数据

另一种显示数据的方法是一次更新一个点,当数据从屏幕上消失时,将其“滚动”回起点。这类似于示波器显示波形的方式,有时被描述为循环缓冲器。

在本例中,使用固定长度阵列创建单个散点图。新值将添加到由跟踪的位置NextIndex,当该位置大于数组时,它会滚动到零。为了使这一点更加明显,通常会在滚动点添加一条垂直线,以便在发生断裂的位置更加明显。

readonly double[] Values = new double[500];
readonly Stopwatch Stopwatch = Stopwatch.StartNew();
readonly ScottPlot.Plottable.VLine VerticalLine;
int NextIndex = 0;

public Form1()
{
    InitializeComponent();
    formsPlot1.Plot.AddSignal(Values);
    VerticalLine = formsPlot1.Plot.AddVerticalLine(0, Color.Red, 2);
    formsPlot1.Plot.SetAxisLimits(0, Values.Length, -2, 2);
}

public void AddDataPoint()
{
    Values[NextIndex] = Math.Sin(Stopwatch.Elapsed.TotalSeconds * 3);

    NextIndex += 1;
    if (NextIndex >= Values.Length)
        NextIndex = 0;

    VerticalLine.X = NextIndex;
}

// This timer adds data frequently (every 1 ms)
private void timer1_Tick(object sender, EventArgs e)
{
    AddDataPoint();
}

// This timer renders infrequently (every 20 ms)
private void timer2_Tick(object sender, EventArgs e)
{
    formsPlot1.Render();
}

使用部分阵列渲染增长数据

您可以创建一个大数组,只显示前N个值,并随着新数据的添加而增加N。这给人一种不断增长的情节错觉,尽管它的来源是一个固定长度的数组。可见值的范围由MinRenderIndexMaxRenderIndex信号图和散点图的字段。

readonly double[] Values = new double[100_000];
readonly ScottPlot.Plottable.SignalPlot SignalPlot;
int NextPointIndex = 0;

public Form1()
{
    InitializeComponent();
    SignalPlot = formsPlot1.Plot.AddSignal(Values);
    formsPlot1.Plot.SetAxisLimits(0, 100, -2, 2);
}

// This timer adds data frequently (1000 times / second)
private void timer1_Tick(object sender, EventArgs e)
{
    Values[NextPointIndex] = Math.Sin(NextPointIndex * .05);
    SignalPlot.MaxRenderIndex = NextPointIndex;
    NextPointIndex += 1;
}

// This timer renders infrequently (10 times per second)
private void timer2_Tick(object sender, EventArgs e)
{
    // adjust the axis limits only when needed
    double currentRightEdge = formsPlot1.Plot.GetAxisLimits().XMax;
    if (NextPointIndex > currentRightEdge)
        formsPlot1.Plot.SetAxisLimits(xMax: currentRightEdge + 100);

    formsPlot1.Render();
}

⚠️ 警告:此示例将在阵列已满时崩溃。要防止这种情况,请在NextPointIndex等于或超过数组的大小,并创建一个新的更大的数组来保存数据。执行此操作时,必须将所有现有值从旧数组复制到新数组。

使用散点图列表增长数据

这个ScatterPlotList绘图类型使用List<double>而不是double[]存储数据,以便Add()数据传输到。添加新数据后,用户可以根据需要设置轴限制并请求渲染。对于大型数据集,散点图绘制速度较慢,因此上述信号绘制方法几乎总是首选。

使用SignalPlotList增长数据

TODO:创建此打印类型