Introduction
ScottPlot is a free and open-source data visualization control written in C#. It can easily achieve interactive visualization of large datasets. The ScottPlot Cookbook teaches us how to create line charts, histograms, pie charts, and scatter plots with just a few lines of code.
ScottPlot Cookbooks Learn how to use ScottPlot.
ScottPlot Demo See what ScottPlot can do.
Quickstart:
- Console Application
- Windows Forms
- WPF
- WinUI
- MAUI
- Uno Platform
- Avalonia
- Eto
- .NET Core API
- Blazor WASM
- PowerShell
- .NET Notebook
- IronPython

Source Code Structure
Tested with 1 million floating-point numbers, the experience is smooth as silk – the chart does not deceive! How cool is that? Let’s quickly look at how this is achieved.
Download source code: GitHub - ScottPlot
Source Code Directory Analysis
Open the downloaded source code; in src there are two versions. We look at the stable version ScottPlot4 (./ScottPlot/src/ScottPlot4/). (Site admin note: The original author wrote this in 2022; as of October 31, 2024, version 5.X has been available for over a year and can be used with confidence.)
The ScottPlot4 directory contains .NET framework components like WinForms and WPF, which can be considered shells for ScottPlot.
The ScottPlot directory is the core directory of this control.
Below is a simplified version of the ScottPlot directory:
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 directory above, analyze from top to bottom:
Control/Backend.cs ----> Backend management, data, settings, events related
Drawing/GDI.cs ----> Low-level drawing interface
Plot/Plot.cs ----> Control API, user-facing
Plottable/IPlottable.cs ----> Components that can be plotted
Renderable/IRenderable.cs ----> Components that can be rendered
Thus, the structure of the ScottPlot directory is quite clear. Mastering these basic concepts makes reading the source code much easier.
Here is a summary diagram:

Source Code Analysis Entry Point
Since the demo uses WinForms, we look at ScottPlot.WinForms/FormsPlot.cs as the entry point for source code analysis.
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;
}
}
In the FormsPlot constructor, a backend Backend is created and event handlers are registered with it. Therefore, Backend events are handled by FormsPlot. The configuration also uses the backend's configuration.
Backend Management
/Control/Backend.cs
public ControlBackEnd(float width, float height, string name = "UnamedControl")
{
Cursor = Configuration.DefaultCursor; // Mouse cursor style
EventFactory = new UIEventFactory(Configuration, Settings, Plot); // Event factory
EventsProcessor = new EventsProcessor(
renderAction: (lowQuality) => Render(lowQuality),
renderDelay: (int)Configuration.ScrollWheelZoomHighQualityDelay); // Event executor
ControlName = name;
Reset(width, height); // At this point width = 1, height = 1, mainly to create a Plot instance
}
/// <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());
Creating a Plot instance here seems a bit odd. Is it to have multiple plots for a single image? If you know, leave a comment!
/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; the previous bitmap is placed into the OldBitmaps queue. The bitmap size is determined by Backend.Resize() in the FormsPlot constructor. Once the bitmap is available, drawing can begin.
ScottPlot Components
Before looking at the implementation of Render(), it's beneficial to have a deeper understanding of the drawing component concepts.
Plot Control API
Plot is the control API, user-facing.
1) Plot Construction
All configuration is stored in settings.
private readonly Settings settings = new Settings();
Ultimately, all user settings are saved in Plot's settings.
2) Setting X, Y Axis 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) settings in Plot/Plot.cs
settings.AxisSet(xMin, xMax, yMin, yMax, xAxisIndex, yAxisIndex);
}
3) Adding 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, // Sampling rate used during rendering
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);
}
Here, SignalPlot is a IPlottable object, which packages the 1 million data points to be displayed into a drawable object. It then calls Add() to put this object into settings.Plottables. settings.Plottables stores 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);
}
When a bitmap update event occurs, Plot.Render.cs:RenderPlottables() is called, which invokes Render() for all objects in settings.Plottables.
/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);
}
From the interface definition, it is similar to IPlottable, likely legacy code.
SignalPlot Rendering Algorithm Analysis
How to display 1 million data points on an image while maintaining smooth interaction when zooming with the mouse? The design concept is simple: sample the 1 million data points according to the X-axis resolution.
SignalPlot inherits from SignalPlotBase but does not override Render(). Therefore, when Plottable.Render() is called, SignalPlotBase's Render() is invoked.
/Plottable/SignalPlotBase.cs
public virtual void Render(PlotDimensions dims, Bitmap bmp, bool lowQuality = false)
{
// Previously initialized in AddSignal(): _SamplePeriod = 1/SampleRate
double dataSpanUnits = _Ys.Length * _SamplePeriod;
double columnSpanUnits = dims.XSpan / dims.DataWidth;
// Number of Y data points per X-axis interval
double columnPointCount = (columnSpanUnits / dataSpanUnits) * _Ys.Length;
// Offset from top-left origin of image to data display area
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;
// Number of points per pixel column
double pointsPerPixelColumn = visiblePointCount / dims.DataWidth;
double dataWidthPx2 = visibleIndex2 - visibleIndex1 + 2;
bool densityLevelsAvailable = DensityLevelCount > 0 && pointsPerPixelColumn > DensityLevelCount;
double firstPointX = dims.GetPixelX(OffsetX); // Convert to pixel
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
// Called when data is numerous
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);
// Serial synchronous method, compute Y data (min/max) for each column on the X axis
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);
}
}
// Get min and max values
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);
}
Alternating Min and Max Values with 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()
{
// Alternately yield min and max
yield return new PointF(x, Min);
yield return new PointF(x, Max);
}
}
Min-Max Search Strategies
There are three strategies in the source code, allowing precomputation or dynamic calculation.
/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/LinearDoubleOnlyMinMaxStrategy.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];
}
}
Events
Different events execute different ProcessEvent() implementations. The design concept is similar to the IPlottable type.
When an event is detected, the corresponding Event is constructed using methods in UIEventFactory.
/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 Flow
First, FormsPlot.cs's PictureBox1 receives mouse events.
Then, it calls Backend.MouseMove().
Finally, the corresponding event's ProcessEvent() is called.
/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);
}
Conclusion
ScottPlot is an excellent software that can handle the display of large amounts of data with powerful performance. It uses the MIT open-source license – truly fantastic!
Previously, I only found articles online introducing and using ScottPlot, but no source code analysis. Today, I am writing a source code analysis of ScottPlot to fill that gap.
I rarely write articles, and I find writing source code analysis quite challenging. Please forgive any shortcomings. I hope you like it!
Finally, thank you to all the contributors of ScottPlot!
Repository: https://github.com/ScottPlot/ScottPlot
Documentation site: https://scottplot.net/