ScottPlotのソース解析

ScottPlotのソース解析

ScottPlotは、C#言語で書かれたフリーでオープンソースのデータビジュアライゼーションコントロールです。大量データの可視化相互作用を容易に実現できる。

最后更新 2024/10/31 5:29
笨免牙
预计阅读 11 分钟
分类
.NET
标签
.NET C# オープンソースソース グラフ一覧 パフォーマンス

プロフィール:プロフィール

ScottPlot是一个免费开源的数据可视化控件,使用C#语言编写。 它可以轻松实现海量数据可视化交互. ScottPlot Cookbook 例程中,教我们如何用几行代码创建线条图,直方图,饼状图,散点图。

Quickstart:

动图封面


ソースコードフレームワーク

用100w浮動小数点数実測,使者如絹,図不是我骗!これはクールだ!どうやってやっているのか見てみましょう

下载源码:GitHub - ScottPlo

ソースコードの解析

打开下载好的源码,在scr中有两个版本,我们看稳定版的ScottPlot4(./ScottPlot/src/ScottPlot4/).(站长注:作者原文写于2022年,目前2024年10月31日5.X版本已上线1年多,可放心使用)

ScottPlot4のディレクトリには、Winforms、WPFなどの. NETアーキテクチャのものがあり、ScottPlotのシェルと考えることができます。

ScottPlotディレクトリは、このコントロールの中核ディレクトリです。

以下は、LiteバージョンのScottPlotディレクトリです。

ScottPlot/
├── AxisLimits.cs
├── Coordinate.cs
├── Control
│   ├── Backend.cs
│   ├── Configuration.cs
│   ├── DisplayScale.cs
│   └── EventProcess
│       ├── EventFactory.cs
│       ├── Events
│       │   ├── RenderHighQuality.cs
│       │   └── RenderLowQuality.cs
│       ├── EventsProcessor.cs
│       └── IUIEvent.cs
├── Drawing
│   ├── Font.cs
│   ├── GDI.cs
│   ├── Palette.cs
│   └── Tools.cs
├── Plot
│   ├── Plot.Add.cs
│   ├── Plot.Axis.cs
│   ├── Plot.cs
│   ├── Plot.Obsolete.cs
│   └── Plot.Render.cs
├── PlotDimensions.cs
├── Plottable
│   ├── AxisLine.cs
│   ├── Image.cs
│   ├── IPlottable.cs
│   ├── MinMaxSearchStrategies
│   │   ├── IMinMaxSearchStrategy.cs
│   │   ├── LinearDoubleOnlyMinMaxStrategy.cs
│   │   ├── LinearFastDoubleMinMaxSearchStrategy.cs
│   │   ├── LinearMinMaxSearchStrategy.cs
│   │   └── SegmentedTreeMinMaxSearchStrategy.cs
│   ├── PiePlot.cs
│   ├── Polygon.cs
│   ├── ScatterPlot.cs
│   ├── SignalPlotBase.cs
│   ├── SignalPlot.cs
│   └──SignalPlotXY.cs
├── README.md
├── Renderable
│   ├── Axis.cs
│   ├── AxisDimensions.cs
│   ├── AxisLabel.cs
│   ├── AxisLine.cs
│   └── IRenderable.cs

基本概念は

上記の目次に基づいて、上から順に分析します。

Control/Backend.cs   	   ----> 后台管理,数据,设置,事件相关
Drawing/GDI.cs		   ----> 底层绘图接口
Plot/Plot.cs    	   ----> 控件API,面向用户
Plottable/IPlottable.cs    ----> 可以绘制的组件
Renderable/IRenderable.cs  ----> 可以渲染的组件

ScottPlotディレクトリの構造は非常に明確です。これらの基本的な概念を保持すると、ソースコードを読むのがはるかに簡単になります。

概要を示す:

img

ソース解析のエントリーポイント

因为Demo用的WinForm,所以我们看ScottPlot.WinForms/FromsPlot.cs,以此为源码分析的入口点。

class FormsPlot : UserControl
{
  public FormsPlot()
    {
        Backend = new Control.ControlBackEnd(1, 1, "FormsPlot");
        Backend.Resize(Width, Height, useDelayedRendering: false);
        Backend.BitmapChanged += new EventHandler(OnBitmapChanged);
        Backend.BitmapUpdated += new EventHandler(OnBitmapUpdated);
        Backend.CursorChanged += new EventHandler(OnCursorChanged);
        Backend.RightClicked += new EventHandler(OnRightClicked);
        Backend.LeftClicked += new EventHandler(OnLeftClicked);
        Backend.LeftClickedPlottable += new EventHandler(OnLeftClickedPlottable);
        Backend.AxesChanged += new EventHandler(OnAxesChanged);
        Backend.PlottableDragged += new EventHandler(OnPlottableDragged);
        Backend.PlottableDropped += new EventHandler(OnPlottableDropped);
        Configuration = Backend.Configuration;
    }
}

FormsPlotの構築時には、バックグラウンドを作成してBackendを管理し、ProxyイベントをBackendに登録するので、BackendのイベントはFormsPlotに引き継がれます。設定はBackendの設定も使用します。

バックグラウンドマネジメント

/Control/Backend.cs

public ControlBackEnd(float width, float height, string name = "UnamedControl")
{
    Cursor = Configuration.DefaultCursor; //鼠标样式
    EventFactory = new UIEventFactory(Configuration, Settings, Plot); //事件构造器
    EventsProcessor = new EventsProcessor(
        renderAction: (lowQuality) => Render(lowQuality),
        renderDelay: (int)Configuration.ScrollWheelZoomHighQualityDelay); //执行事件
    ControlName = name;
    Reset(width, height);// 此时是width = 1,height =1,主要用于创建Plot实例
}
/// <summary>
/// Reset the back-end by creating an entirely new plot of the given dimensions
/// </summary>
public void Reset(float width, float height) => Reset(width, height, new Plot());

この場所でPlotインスタンスを作成するのは少し奇妙です。1枚の画像に複数のプロットを?知っている人はコメントを残す!

/Control/Backend.cs

public void Reset(float width, float height, Plot newPlot)
{
    Plot = newPlot;
    Settings = Plot.GetSettings(false);
    EventFactory = new UIEventFactory(Configuration, Settings, Plot);
    WasManuallyRendered = false;
    Resize(width, height, useDelayedRendering: false);
}
public void Resize(float width, float height, bool useDelayedRendering)
{
    // Disposing a Bitmap the GUI is displaying will cause an exception.
    // Keep track of old bitmaps so they can be disposed of later.
    OldBitmaps.Enqueue(Bmp);
    Bmp = new System.Drawing.Bitmap((int)width, (int)height);
    BitmapRenderCount = 0;

    if (useDelayedRendering)
        RenderRequest(RenderType.HighQualityDelayed);
    else
        Render();
}

描画用のBitmapを作成します。前のBitmapはOldキューに入れられます。BitmapサイズはFormsPlot时Backend.Resizeによってされる. Bitmapがあれば、上に描くことができます。

ScottPlotコンポーネント

在看Render()这个函数实现前,还是先把关于绘制组件概念理解更深入一些。

PlotコントロールAPI

PlotはコントロールAPIであり,ユーザ向けである.

**1 Plot **

設定はすべて設定にあります。

private readonly Settings settings = new Settings();

最終的に、ユーザ設定はPlotのsettingsに保存されます。

**2 x、y轴のサイズ范囲をする **

/Plot/Plot.Axis.cs

public void SetAxisLimits(double? xMin = null, double? xMax = null,double? yMin = null, 
    double? yMax = null,int xAxisIndex = 0, int yAxisIndex = 0)
{
     //1)Plot/Plot.cs的settings
     settings.AxisSet(xMin, xMax, yMin, yMax, xAxisIndex, yAxisIndex);
}

**3 Y轴信号を1つ追加 **

/Plot/Plot.Add.cs

public SignalPlot AddSignal(double[] ys, double sampleRate = 1, Color? color = null, string label = null)
{
    SignalPlot signal = new SignalPlot()
    {
        Ys = ys,
        SampleRate = sampleRate, //渲染时,将用到采样速度
        Color = color ?? settings.GetNextColor(),
        Label = label,

        // TODO: FIX THIS!!!
        MinRenderIndex = 0,
        MaxRenderIndex = ys.Length - 1,
    };
    Add(signal); 
    return signal
}
public void Add(IPlottable plottable)
{
    settings.Plottables.Add(plottable);
}

ここでSignalPlotはPlottableオブジェクトで、表示する100wデータを描画可能なオブジェクトにパッケージ化したものです。

Addを呼び出して、このオブジェクトをings. Plottに入れます。settings.Plottablesは描画するすべてのオブジェクトを格納します。

**4レンダリング関数 ***

/Plot/Plot.Render.cs

public interface IPlottable
{
    /// <summary>
    /// Controls whether the plot will be rendered and contribute to automatic axis limit detection
    /// </summary>
    bool IsVisible { get; set; }

    /// <summary>
    /// Index of the horizontal axis this plottable will use for coordinate/pixel conversions.
    /// 0 is the bottom axis, 1 is the top axis, and higher numbers are additional custom axes.
    /// </summary>
    int XAxisIndex { get; set; }

    /// <summary>
    /// Index of the vertical axis this plottable will use for coordinate/pixel conversions.
    /// 0 is the left axis, 1 is the right axis, and higher numbers are additional custom axes.
    /// </summary>
    int YAxisIndex { get; set; }

    /// <summary>
    /// This is called when it is time to draw the plottable on the canvas.
    /// </summary>
    /// <param name="dims">Spatial information about the plot and all axes to assist with coordinate/pixel conversions.</param>
    /// <param name="bmp">The image on which this plottable will be drawn.</param>
    /// <param name="lowQuality">If true, disable anti-aliased lines and text to achieve faster rendering.</param>
    void Render(PlotDimensions dims, System.Drawing.Bitmap bmp, bool lowQuality = false);
}

当更新Bitmap事件产生时,将通过Plot.Render.cs:RenderPlottables()函数, 调用settings.Plottables中所有对象的Render().

/Plot/Plot.Render.cs

private void RenderPlottables(Bitmap bmp, bool lowQuality, double scaleFactor)
{
    foreach (var plottable in settings.Plottables)
   {
      if (plottable.IsVisible == false)
           continue;
      plottable.Render(dims, bmp, lowQuality);
   }
 }

Renderable

public interface IRenderable
{
    bool IsVisible { get; set; }
    void Render(PlotDimensions dims, Bitmap bmp, bool lowQuality = false);
}

インターフェイス定義から分かるように、IPlottableに似ており、古いコードである可能性が高い。


SignalPlotレンダリングアルゴリズムの解析

100w データを画像に表示し、マウスの動きでズームインしても滑らかな感触を維持するには?その設計思想はとても簡単で、それは ** 100wデータに対してx軸分解能でサンプリングする ** である。

SignalPlot继承自SignalPlotBase,但未覆盖Render()。因此,当Plottable.Render()被调用,即调用SignalPlotbase的Render().

/Plottable/SignalPlotBase.cs

public virtual void Render(PlotDimensions dims, Bitmap bmp, bool lowQuality = false)
{
   //之前AddSignal()中初始化 _SamplePeriod = 1/SampleRata
   double dataSpanUnits = _Ys.Length * _SamplePeriod;	
   double columnSpanUnits = dims.XSpan / dims.DataWidth;
   //x轴中每个间隔中包含的Y数据个数
   double columnPointCount = (columnSpanUnits / dataSpanUnits) * _Ys.Length;
   //OffsetX图片左上原点到数据显示区的偏移
   double offsetUnits = dims.XMin - OffsetX;				
   double offsetPoints = offsetUnits / _SamplePeriod;
   int visibleIndex1 = (int)(offsetPoints);
   int visibleIndex2 = (int)(offsetPoints + columnPointCount * (dims.DataWidth + 1));
   int visiblePointCount = visibleIndex2 - visibleIndex1;
   //x轴中每个间隔中包含的点个数
   double pointsPerPixelColumn = visiblePointCount / dims.DataWidth;           
   double dataWidthPx2 = visibleIndex2 - visibleIndex1 + 2;
   bool densityLevelsAvailable = DensityLevelCount > 0 && pointsPerPixelColumn > DensityLevelCount;
   double firstPointX = dims.GetPixelX(OffsetX);//转换成像素点
   double lastPointX = dims.GetPixelX(_SamplePeriod * (_Ys.Length - 1) + OffsetX);
   double dataWidthPx = lastPointX - firstPointX;
   double columnsWithData = Math.Min(dataWidthPx, dataWidthPx2);

   if (columnsWithData < 1 && Ys.Length > 1)
   {
      RenderSingleLine(dims, gfx, penHD);
    }
    else if (pointsPerPixelColumn > 1 && Ys.Length > 1)
    {
      if (densityLevelsAvailable)
          RenderHighDensityDistributionParallel(dims, gfx, offsetPoints, columnPointCount);
      else
	 //数据多的情况调用此函数
         RenderHighDensity(dims, gfx, offsetPoints, columnPointCount, penHD);
    }
    else
    {
        RenderLowDensity(dims, gfx, visibleIndex1, visibleIndex2, brush, penLD, penHD);
    }
}

private void RenderHighDensity(PlotDimensions dims, Graphics gfx, double offsetPoints, double columnPointCount, Pen penHD)
{
    int dataColumnFirst = (int)Math.Ceiling((-1 - offsetPoints + MinRenderIndex) / columnPointCount - 1);
    int dataColumnLast = (int)Math.Ceiling((MaxRenderIndex - offsetPoints) / columnPointCount);

    var columns = Enumerable.Range(dataColumnFirst, dataColumnLast - dataColumnFirst);

    //串行同步方式,对x轴每一列中的Y数据进行计算(取最大最小值)
    intervals = columns
          .Select(xPx => CalcInterval(xPx, offsetPoints, columnPointCount, dims));

    PointF[] linePoints = intervals
          .SelectMany(c => c.GetPoints())
          .ToArray();

    for (int i = 0; i < linePoints.Length; i++)
        linePoints[i].X += dims.DataOffsetX;

    if (linePoints.Length > 0)
    {
        ValidatePoints(linePoints);
        gfx.DrawLines(penHD, linePoints);
    }
}

//取最大值,最小值
private IntervalMinMax CalcInterval(int xPx, double offsetPoints, double columnPointCount, PlotDimensions dims)
{
    // get the min and max value for this column                
    Strategy.MinMaxRangeQuery(index1, index2, out double lowestValue, out double highestValue);

    float yPxHigh = dims.GetPixelY(lowestValue + OffsetYAsDouble);
    float yPxLow = dims.GetPixelY(highestValue + OffsetYAsDouble);
    return new IntervalMinMax(xPx, yPxLow, yPxHigh);
}

GetPointsを使用して最大値、最小値を交互に取る

private class IntervalMinMax
{
    public float x;
    public float Min;
    public float Max;
    public IntervalMinMax(float x, float Min, float Max)
    {
        this.x = x;
        this.Min = Min;
        this.Max = Max;
     }
    public IEnumerable<PointF> GetPoints()
    {
	//交替取最大值,最小
        yield return new PointF(x, Min);
        yield return new PointF(x, Max);
    }
}

最大最小値ポリシーの検索

ソースコードには3つの戦略があり、事前に計算でき、動的に計算できます。

/Plottable/MinMaxSearchStrategy/IMinMaxSearchStrategy.cs

public interface IMinMaxSearchStrategy<T>
{
    T[] SourceArray { get; set; }
    void MinMaxRangeQuery(int l, int r, out double lowestValue, out double highestValue);
    void updateElement(int index, T newValue);
    void updateRange(int from, int to, T[] newData, int fromData = 0);
    double SourceElement(int index);
}

/Plottable/MinMaxSearchStrategy/LinearDoubleOnlyMinMaxStrateg.cs

public void MinMaxRangeQuery(int l, int r, out double lowestValue, out double highestValue)
{
    lowestValue = sourceArray[l];
     highestValue = sourceArray[l];
     for (int i = l; i <= r; i++)
     {
	if (sourceArray[i] < lowestValue)
		lowestValue = sourceArray[i];
	if (sourceArray[i] > highestValue)
		highestValue = sourceArray[i];
      }
}

イベントイベント

不同的事件,执行不同的ProcessEvent()。和Plottable类型设计思想相同。

当检测到事件,用UIEventFactor中的方法构造相应的Event.

/Control/EventProcess/Events/IUIEvent.cs

public interface IUIEvent
{
   public RenderType RenderType { get; }
   void ProcessEvent();
}

/Control/EventProcess/Events/MouseZoomEvent.cs

public void ProcessEvent()
{
   float x = Input.ShiftDown ? Settings.MouseDownX : Input.X;
   float y = Input.CtrlDown ? Settings.MouseDownY : Input.Y;
   Settings.MouseZoom(x, y);
}

イベント実行フロー

首先,是FormsPlot.cs中PictureBox1接收鼠标事件。

然后,调用Backend.MouseMove().

最后,调用事件对应的ProcessEvent().

/ScottPlot.Winforms/FormsPlot.cs

private void PictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    Backend.MouseMove(GetInputState(e)); base.OnMouseMove(e);
}
public void MouseMove(InputState input)
{
     mouseMoveEvent = EventFactory.CreateMouseZoom(input);
     ProcessEvent(mouseMoveEvent);
}

終わりだ

ScottPlot是一款很棒的软件,可以解决大量数据显示问题,性能强悍。使用MIT 开源协议,真香!

之前在网上只搜索到ScottPlot介绍和使用的文章,没有源码分析.今天我来写一个ScottPlot源码分析,补充一下。

めったに記事を書く、ソースコード分析の記事を書くのは非常に難しい、欠点を許してください!皆さん楽しんでください!

最后,感谢ScottPlot的贡献者们!

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/04/22

バージョン別の. NETサポート状況(250 7 0 7更新)

仮想マシンとテストマシンを使用して、各バージョンのオペレーティングシステムの. NETサポートをテストします。オペレーティングシステムのインストール後、対応するランタイムを測定し、スターダストエージェントをパスとして実行できます。

继续阅读
同分类 / 同标签 2026/02/07

AOTの使用経験

プロジェクトの最初から、新しい機能が追加されたり、新しい構文が使用されたりするたびに、AOTリリーステストを行うという良い習慣を身につける必要があります。

继续阅读