ScottPlot source code analysis

ScottPlot source code analysis

ScottPlot is a free and open source data visualization control written in C#language. It can easily realize visualization and interaction of massive data.

最后更新 10/31/2024 5:29 AM
笨免牙
预计阅读 13 分钟
分类
.NET
标签
.NET C# open source chart performance

profile

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

Quickstart:

动图封面


source frameworks

Using a 100w floating point number to measure, the messenger is like silk, and the picture does not deceive me! That's so cool! Then let's hurry and see how this is done?

下载源码:GitHub - ScottPlo

Source code catalog analysis

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

There are Winforms,WPF and other. NET architecture things in the directory of ScottPlot4, which can be used as a shell of ScottPlot.

The ScottPlot directory is the core directory for this control.

Here is a simplified version of the ScottPlot catalog:

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

basic concepts

Based on the above catalog, analyze it in order from top to bottom:

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

It seems that the structure of the ScottPlot directory is still very clear. With these basic concepts in mind, reading the source code becomes much easier.

Here is a picture summary:

img

Source code analysis entry point

因为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;
    }
}

When FormsPlot is constructed, a background management Backend is created and proxy events are registered in the Backend, so the events of the Backend are taken over by FormsPlot. Configuration also uses the configuration in Backend.

background management

/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());

It still feels a bit strange to create a Plot instance in this place. Do you plan multiple plots for one picture? Anyone who knows, leave a message!

/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();
}

Create a Bitmap for drawing, and the previous Bitmap is placed in the Old queue. The Bitmap size is determined by Backend.Resize() when FormsPlot is constructed. With Bitmap, you can draw on it.

ScottPlot component

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

Plot Control API

Plot is a control API and is user-oriented.

**1)Plot construction **

All the settings are in settings.

private readonly Settings settings = new Settings();

Finally, user settings are saved in Plot settings.

**2) Set x,y axis size range **

/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) Add a Y-axis signal **

/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);
}

The SignalPlot here is a Plottable object, which packs 100w of data to be displayed into a drawable object.

And call Add() to place this object in settings.Plottables. Settings.Plottables store all objects to be drawn.

**4) Render function **

/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);
}

It can be seen from the interface definition that it is similar to IPlottable, but it is likely to be old code.


Analysis of SignalPlot rendering algorithm

How to display ** 100w ** data on a picture and maintain a silky feel when the mouse moves to zoom in? The design idea is very simple, that is, ** sample 100w data at x-axis resolution **.

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);
}

Use GetPoints() to alternately take the maximum and minimum values

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);
    }
}

Find max and min policy

There are three strategies in the source code, which can be calculated in advance or dynamically.

/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];
      }
}

event

不同的事件,执行不同的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);
}

Event execution process

首先,是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);
}

end

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

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

I rarely write articles, but I find it quite difficult to write articles on source code analysis. Please forgive me for any shortcomings! Hope everyone likes it!

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

Keep Exploring

延伸阅读

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

Support for. NET by operating system versions (250707 update)

Use virtual machines and test machines to test the support of each version of the operating system for. NET. After installing the operating system, it is passed by measuring the corresponding running time of the installation and being able to run the Stardust Agent.

继续阅读
同分类 / 同标签 2/7/2026

Summary of experience in using AOT

From the very beginning of project creation, you should develop a good habit of conducting AOT release testing in a timely manner whenever new features are added or newer syntax is used.

继续阅读