There is a bug in the "DataLogger Add Marker" feature?

Open
#5,166 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
35/100
Issue type
Bug
Clarity
Needs clarification
Activity status
Quiet
Tech stack
csharp

Research direction

Start with the supplied FrmInstantPCR WinForms code and the DataLogger Add Marker interaction in ScottPlot 5.1.57 on .NET Framework 4.8. Reproduce the marker action while monitoring the plotted series, then identify which displayed line segments are excess and verify that the corrected behavior shows only the intended marker and data lines.

Written by the indexing model from the issue text.

Description

After DataLogger Add Marker, there are excess line segments present

ScottPlot 5.1.57,winform,.net framework4.8

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using log4net;
using ScottPlot;
using ScottPlot.Plottables;
using MaInterface.Models;
using PCR.Common;
using PCR.Model;
using PCR.BLL;

namespace PCRApp
{
public partial class FrmInstantPCR : AntdUI.BorderlessForm
{
private const int MAX_HOLE_COUNT = 8;
private const int MAX_MARKER_COUNT = 8;

    private Form _chartToolTipForm = null;
    private Marker _chartHighlightMarker = null;
    private readonly int _moduleNum = 1;
    private readonly string _projectId = "";
    private readonly Timer AddNewDataTimer = null;
    private readonly Timer UpdatePlotTimer = null;
    private readonly ConcurrentQueue<PcrModel> _pcrDataQueue = null;
    private readonly ConcurrentQueue<SingleDateTimeTemperatureModel> _temperatureDataQueue = null;
    private readonly List<ProjectRVDI> _projectRVDIList = new List<ProjectRVDI>();
    private readonly Dictionary<string, DataLogger> _originLoggerList = new Dictionary<string, DataLogger>(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, DataLogger> _temperatureLoggerList = new Dictionary<string, DataLogger>(StringComparer.OrdinalIgnoreCase);
    private readonly ILog log = LogManager.GetLogger(typeof(FrmInstantPCR));

    public string ModuleCode
    {
        get
        {
            return Constants.MODULE_CODE[(_moduleNum - 1) % Constants.MODULE_CODE.Length].ToUpper().Trim();
        }
    }

    public FrmInstantPCR(ConcurrentQueue<PcrModel> initPcrDataQueue, ConcurrentQueue<SingleDateTimeTemperatureModel> initTemperatureQueue, int moduleNum, string projectId)
    {
        InitializeComponent();
        AddNewDataTimer = new Timer() { Interval = 40 };
        UpdatePlotTimer = new Timer() { Interval = 90 };

        temperatureChart.UserInputProcessor.IsEnabled = false;
        originChart.UserInputProcessor.IsEnabled = false;

        tabMain.SelectedIndex = 1;
        _projectId = projectId;
        _moduleNum = moduleNum < 1 ? 1 : moduleNum;
        _pcrDataQueue = new ConcurrentQueue<PcrModel>();
        _temperatureDataQueue = new ConcurrentQueue<SingleDateTimeTemperatureModel>();
        this.Text = pageHeader.Text = $"实时温度、荧光曲线 - 模块{ModuleCode}";

        // 订阅事件,实时接收数据
        EventAggregator.PcrModelChangedEvent += EventAggregator_PcrModelChanged;
        EventAggregator.SingleTemperatureModelChangedEvent += EventAggregator_SingleTemperatureModelChangedEvent;

        InitHoleCheckboxes();
        InitMarkerCheckboxes();
        AutoCheckMarker();
        InitOriginLogger();
        InitTemperatureLogger();

        FormClosing += FrmInstantPCR_FormClosing;
        AddNewDataTimer.Tick += AddNewDataTimer_Tick;
        UpdatePlotTimer.Tick += UpdatePlotTimer_Tick;

        Task.Run(() =>
        {
            try
            {
                // 初始化历史温度数据
                while (initTemperatureQueue != null && initTemperatureQueue.IsEmpty == false)
                {
                    if (initTemperatureQueue.TryDequeue(out var temperature))
                    {
                        if (temperature.CurrSingleTemperatureModel.ModuleNum == _moduleNum)
                        {
                            double seconds = temperature.RunTime.TotalSeconds;
                            TimeSpan timeSpan = TimeSpan.FromSeconds(seconds);
                            log.Debug($"init===>seconds: {seconds}, time: {(int)timeSpan.TotalMinutes:00}:{timeSpan.Seconds:00}");
                            _temperatureLoggerList["Env"].Add(seconds, temperature.CurrSingleTemperatureModel.EnvTemp);
                            _temperatureLoggerList["Lid"].Add(seconds, temperature.CurrSingleTemperatureModel.LidTemp);
                            _temperatureLoggerList["Tec1"].Add(seconds, temperature.CurrSingleTemperatureModel.Block1Temp);
                            _temperatureLoggerList["Tec2"].Add(seconds, temperature.CurrSingleTemperatureModel.Block2Temp);
                        }
                    }
                }

                // 初始化历史荧光数据
                while (initPcrDataQueue != null && initPcrDataQueue.IsEmpty == false)
                {
                    if (initPcrDataQueue.TryDequeue(out var model))
                    {
                        foreach (var item in _projectRVDIList)
                        {
                            string key = $"{ModuleCode}{item.TubeID}|{item.MarkerName}".Trim();
                            if (_originLoggerList.ContainsKey(key))
                            {
                                var dataLogger = _originLoggerList[key];
                                if (dataLogger != null)
                                {
                                    log.Debug($"init===>key: {key}, Cycle: {model.Cycle}");
                                    _originLoggerList[key].Add(model.Cycle, GetPcrValue(model, item.TubeID, item.MarkerName));
                                }
                            }
                        }
                    }
                }

                Task.Delay(500).Wait(); // 等待图表初始化完成
                if (this.IsHandleCreated)
                {
                    this.Invoke(() =>
                    {
                        AddNewDataTimer.Start();
                        UpdatePlotTimer.Start();
                        originChart.MouseMove += Chart_MouseMove;
                        temperatureChart.MouseMove += Chart_MouseMove;
                    });
                }
            }
            catch (Exception ex)
            {
                log.Error("初始化实时数据异常", ex);
            }
        });
    }

    private void FrmInstantPCR_FormClosing(object sender, FormClosingEventArgs e)
    {
        AddNewDataTimer.Stop();
        UpdatePlotTimer.Stop();

        // 等待可能正在执行的 Tick 完成
        System.Threading.Thread.Sleep(50);

        EventAggregator.PcrModelChangedEvent -= EventAggregator_PcrModelChanged;
        EventAggregator.SingleTemperatureModelChangedEvent -= EventAggregator_SingleTemperatureModelChangedEvent;
        originChart.MouseMove -= Chart_MouseMove;
        temperatureChart.MouseMove -= Chart_MouseMove;
        AddNewDataTimer.Tick -= AddNewDataTimer_Tick;
        UpdatePlotTimer.Tick -= UpdatePlotTimer_Tick;
        pnlHoles.Controls.OfType<AntdUI.Checkbox>().ToList().ForEach(x => x.Click -= ChkFilter_Click);
        pnlMarkers.Controls.OfType<AntdUI.Checkbox>().ToList().ForEach(x => x.Click -= ChkFilter_Click);

        AddNewDataTimer.Dispose();
        UpdatePlotTimer.Dispose();
    }

    [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
    private void EventAggregator_SingleTemperatureModelChangedEvent(SingleDateTimeTemperatureModel model)
    {
        if (model == null || model.CurrSingleTemperatureModel.ModuleNum != _moduleNum)
        {
            return;
        }

        try
        {
            _temperatureDataQueue.Enqueue(model);
        }
        catch (Exception ex)
        {
            log.Error("实时温度显示异常", ex);
            Utils.AntdAlert(this, "系统提示", "实时温度显示异常!", AntdUI.TType.Error);
        }
    }

    [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
    private void EventAggregator_PcrModelChanged(PcrModel model)
    {
        if (model == null || model.Cycle < 0 || model.ModuleNum != _moduleNum)
        {
            return;
        }

        try
        {
            _pcrDataQueue.Enqueue(model);
        }
        catch (Exception ex)
        {
            log.Error("实时荧光显示异常", ex);
            Utils.AntdAlert(this, "系统提示", "实时荧光显示异常!", AntdUI.TType.Error);
        }
    }

    [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
    private void AddNewDataTimer_Tick(object sender, EventArgs e)
    {
        try
        {
            bool isValidTemperatureDataQueue = _temperatureDataQueue != null && _temperatureDataQueue.IsEmpty == false;
            if (isValidTemperatureDataQueue && _temperatureDataQueue.TryDequeue(out var temperature))
            {
                if (temperature.CurrSingleTemperatureModel.ModuleNum == _moduleNum)
                {
                    double seconds = temperature.RunTime.TotalSeconds;
                    TimeSpan timeSpan = TimeSpan.FromSeconds(seconds);
                    log.Debug($"add===>seconds: {seconds}, time: {(int)timeSpan.TotalMinutes:00}:{timeSpan.Seconds:00}");
                    _temperatureLoggerList["Env"].Add(seconds, temperature.CurrSingleTemperatureModel.EnvTemp);
                    _temperatureLoggerList["Lid"].Add(seconds, temperature.CurrSingleTemperatureModel.LidTemp);
                    _temperatureLoggerList["Tec1"].Add(seconds, temperature.CurrSingleTemperatureModel.Block1Temp);
                    _temperatureLoggerList["Tec2"].Add(seconds, temperature.CurrSingleTemperatureModel.Block2Temp);
                }
            }

            bool isValidPcrDataQueue = _pcrDataQueue != null && _pcrDataQueue.IsEmpty == false;
            if (isValidPcrDataQueue && _pcrDataQueue.TryDequeue(out var model))
            {
                foreach (var item in _projectRVDIList)
                {
                    string key = $"{ModuleCode}{item.TubeID}|{item.MarkerName}".Trim();
                    if (_originLoggerList.ContainsKey(key))
                    {
                        var dataLogger = _originLoggerList[key];
                        if (dataLogger != null)
                        {
                            log.Debug($"add===>key: {key}, Cycle: {model.Cycle}");
                            _originLoggerList[key].Add(GetPcrValue(model, item.TubeID, item.MarkerName));
                        }
                    }
                }
            }
        }
        catch (Exception ex)
        {
            log.Error("添加实时数据异常", ex);
            Utils.ShowMessage(this, "添加实时数据异常", AntdUI.TType.Error, true);
        }
    }

    [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
    private void UpdatePlotTimer_Tick(object sender, EventArgs e)
    {
        try
        {
            if (_temperatureLoggerList.Any(x => x.Value.HasNewData))
            {
                temperatureChart.Refresh();
            }
            if (_originLoggerList.Any(x => x.Value.HasNewData))
            {
                originChart.Refresh();
            }
        }
        catch (Exception ex)
        {
            log.Error("刷新图表异常", ex);
            Utils.ShowMessage(this, "刷新图表异常", AntdUI.TType.Error, true);
        }
    }

    private void ChkFilter_Click(object sender, EventArgs e)
    {
        try
        {
            FilterSeries();
            originChart.Refresh();
        }
        catch (Exception ex)
        {
            log.Error("筛选荧光曲线异常", ex);
            Utils.AntdAlert(this, "系统提示", "筛选荧光曲线异常!", AntdUI.TType.Error);
        }
    }

    [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
    private void Chart_MouseMove(object sender, MouseEventArgs e)
    {
        try
        {
            var formsPlot = (ScottPlot.WinForms.FormsPlot)sender;
            (DataLogger dataLogger, DataPoint dataPoint) = GetNearestPoint(formsPlot.Plot, e.Location.X, e.Location.Y);
            if (dataLogger == null)
            {
                return;
            }

            // 清除上次的提示和标记
            _chartToolTipForm?.Close();
            _chartToolTipForm?.Dispose();
            _chartToolTipForm = null;
            formsPlot.Plot.GetPlottables<Marker>().ToList().ForEach(x => formsPlot.Plot.Remove(x));

            // 重新创建提示和标记
            //_chartHighlightMarker = formsPlot.Plot.Add.Marker(0, 0);
            //_chartHighlightMarker.Shape = MarkerShape.OpenCircle;
            //_chartHighlightMarker.Size = 15;
            //_chartHighlightMarker.LineWidth = 2;
            //_chartHighlightMarker.IsVisible = true;
            //_chartHighlightMarker.Location = dataPoint.Coordinates;
            //_chartHighlightMarker.MarkerStyle.LineColor = dataLogger.MarkerStyle.FillColor;

            string text = $"{dataLogger.LegendText}\r\nCycle: {dataPoint.X}\r\nOrigin: {dataPoint.Y:N4}";
            if (formsPlot.Equals(temperatureChart))
            {
                TimeSpan timespan = TimeSpan.FromSeconds(dataPoint.X);
                string timeString = $"{(int)timespan.TotalMinutes:00}:{timespan.Seconds:00}";
                text = $"{timeString}\r\n{dataLogger.LegendText}  {dataPoint.Y:N2}";
            }

            _chartToolTipForm = AntdUI.Tooltip.open(new AntdUI.Tooltip.Config(formsPlot, text)
            {
                Offset = new System.Drawing.Rectangle(e.Location.X, e.Location.Y, 0, 0),
                Back = System.Drawing.Color.FromArgb(220, dataLogger.Color.R, dataLogger.Color.G, dataLogger.Color.B),
                Fore = System.Drawing.Color.White
            });
        }
        catch (Exception ex)
        {
            log.Error("图表鼠标移动事件异常", ex);
            Utils.AntdAlert(this, "系统提示", "图表操作异常[1]!", AntdUI.TType.Error);
        }
    }

    private (DataLogger dataLogger, DataPoint point) GetNearestPoint(Plot plot, double mouseX, double mouseY)
    {
        Pixel mousePixel = new Pixel(mouseX, mouseY);
        Coordinates mouseLocation = plot.GetCoordinates(mousePixel);
        List<DataLogger> allDataLoggerList = plot.GetPlottables<DataLogger>().Where(x => x.IsVisible).ToList();
        Dictionary<int, DataPoint> nearestPoints = new Dictionary<int, DataPoint>();
        for (int i = 0; i < allDataLoggerList.Count; i++)
        {
            DataPoint nearestPoint = allDataLoggerList[i].GetNearest(mouseLocation, plot.LastRender.DataRect);
            nearestPoints.Add(i, nearestPoint);
        }

        int index = -1;
        bool pointSelected = false;
        double smallestDistance = double.MaxValue;
        for (int i = 0; i < nearestPoints.Count; i++)
        {
            if (nearestPoints[i].IsReal)
            {
                double distance = nearestPoints[i].Coordinates.Distance(mouseLocation);
                if (distance < smallestDistance)
                {
                    index = i;
                    pointSelected = true;
                    smallestDistance = distance;
                }
            }
        }

        if (pointSelected)
        {
            ScottPlot.Plottables.DataLogger dataLogger = allDataLoggerList[index];
            DataPoint nearest = nearestPoints[index];
            return (dataLogger, nearest);

        }
        return (null, DataPoint.None);
    }

    private float GetPcrValue(PcrModel model, int holeNum, string marker)
    {
        if (model == null || holeNum < 1 || holeNum >= model.FamPcr.Count)
        {
            return 0f;
        }

        switch (marker.ToLower().Trim())
        {
            case "fam":
                return model.FamPcr[holeNum - 1];
            case "vic":
                return model.VicPcr[holeNum - 1];
            case "rox":
                return model.RoxPcr[holeNum - 1];
            case "cy5":
                return model.Cy5Pcr[holeNum - 1];
            case "cy55":
                return model.Cy55Pcr[holeNum - 1];
            case "tamra":
            case "att0425":
                return model.TamraPcr[holeNum - 1];
            case "cy7":
                return model.Cy7Pcr[holeNum - 1];
            case "cy8":
            case "af405":
                return model.Cy8Pcr[holeNum - 1];
            default:
                return 0f;
        }
    }

    private void FilterSeries()
    {
        var checkedHoleList = pnlHoles.Controls.OfType<AntdUI.Checkbox>()
            .Where(x => x.Checked).Select(x => x.Text.Trim()).ToList();
        var checkedMarkerList = pnlMarkers.Controls.OfType<AntdUI.Checkbox>()
            .Where(x => x.Checked).Select(x => x.Text.Trim()).ToList();
        var allSeries = originChart.Plot.GetPlottables<DataLogger>();
        var filterSeriesList = allSeries.Where(item =>
        {
            var parts = ParseLegendForFilters(item.LegendText);
            if (parts.Count < 2)
            {
                return false;
            }
            return checkedHoleList.Contains(parts[0], StringComparer.OrdinalIgnoreCase) &&
                checkedMarkerList.Contains(parts[1], StringComparer.OrdinalIgnoreCase);
        }).ToList();

        foreach (var series in allSeries)
        {
            bool visible = filterSeriesList.Contains(series);
            series.IsVisible = visible;
        }
    }

    private void AutoCheckMarker()
    {
        TResult<Projects> result = ProjectsBLL.Instance.GetByID(_projectId, true);
        if (result.Code != 0)
        {
            Utils.AntdAlert(this, "系统提示", "获取项目详情失败,无法自动选择荧光通道!", AntdUI.TType.Error);
            return;
        }

        var allRVDIList = result.Data.ProjectRVDIList;
        var allCheckMarker = pnlMarkers.Controls.OfType<AntdUI.Checkbox>().ToList();
        foreach (var item in allRVDIList)
        {
            var checkbox = allCheckMarker.FirstOrDefault(x => String.Equals(x.Text.Trim(),
                item.MarkerName.Trim(), StringComparison.OrdinalIgnoreCase));
            _projectRVDIList.Add(item);

            if (checkbox != null)
            {
                checkbox.Checked = true;
            }
        }
    }

    private void InitOriginLogger()
    {
        for (int i = 0; i < _projectRVDIList.Count; i++)
        {
            var currRVDI = _projectRVDIList[i];
            var key = $"{ModuleCode}{currRVDI.TubeID}|{currRVDI.MarkerName}".Trim();
            var legendText = $"{ModuleCode}{currRVDI.TubeID}|{currRVDI.TargetName}【{currRVDI.MarkerName}】".Trim();
            if (!_originLoggerList.ContainsKey(key))
            {
                DataLogger logger = originChart.Plot.Add.DataLogger();
                var colorConfig = Constants.OrderlyPcrs.FirstOrDefault(x => String.Equals(x.Marker.Trim(),
                    currRVDI.MarkerName, StringComparison.OrdinalIgnoreCase));
                var color = Color.FromColor(System.Drawing.Color.FromArgb(colorConfig.R, colorConfig.G, colorConfig.B));
                logger.LineColor = color;
                logger.LineWidth = 2f;
                logger.Data.XOffset = 1;
                logger.MarkerSize = 5f;
                logger.MarkerFillColor = color;
                logger.LegendText = legendText;
                _originLoggerList.Add(key, logger);
            }
        }
        originChart.Plot.HideLegend();
    }

    private void InitTemperatureLogger()
    {
        DataLogger envLogger = temperatureChart.Plot.Add.DataLogger();
        envLogger.LineColor = Color.FromColor(System.Drawing.Color.FromArgb(22, 118, 255));
        envLogger.LegendText = "Env";
        _temperatureLoggerList.Add("Env", envLogger);

        DataLogger lidLogger = temperatureChart.Plot.Add.DataLogger();
        lidLogger.LineColor = Color.FromColor(System.Drawing.Color.FromArgb(247, 70, 88));
        lidLogger.LegendText = "Lid";
        _temperatureLoggerList.Add("Lid", lidLogger);

        DataLogger tecLogger1 = temperatureChart.Plot.Add.DataLogger();
        tecLogger1.LineColor = Color.FromColor(System.Drawing.Color.FromArgb(82, 194, 26));
        tecLogger1.LegendText = "Tec1";
        _temperatureLoggerList.Add("Tec1", tecLogger1);

        DataLogger tecLogger2 = temperatureChart.Plot.Add.DataLogger();
        tecLogger2.LineColor = Color.FromColor(System.Drawing.Color.FromArgb(23, 134, 0));
        tecLogger2.LegendText = "Tec2";
        _temperatureLoggerList.Add("Tec2", tecLogger2);

        // 将 X 轴刻度值视作秒并格式化为 mm:ss
        var xaxis = temperatureChart.Plot.Axes.NumericTicksBottom();
        xaxis.TickGenerator = new ScottPlot.TickGenerators.NumericAutomatic()
        {
            LabelFormatter = seconds =>
            {
                try
                {
                    if (double.IsNaN(seconds) || double.IsInfinity(seconds))
                    {
                        return "00:00";
                    }
                    var timeSpan = TimeSpan.FromSeconds(seconds);
                    return $"{(int)timeSpan.TotalMinutes:00}:{timeSpan.Seconds:00}";
                }
                catch
                {
                    return "00:00";
                }
            }
        };

        temperatureChart.Plot.HideLegend();
    }

    private void InitHoleCheckboxes()
    {
        pnlHoles.SuspendLayout();
        pnlHoles.Controls.Clear();
        string moduleCode = Constants.MODULE_CODE[(_moduleNum - 1) % Constants.MODULE_CODE.Length];
        for (int i = 1; i <= MAX_HOLE_COUNT; i++)
        {
            int positionX = 10;
            if (pnlHoles.Controls.Count > 0)
            {
                positionX = pnlHoles.Controls[pnlHoles.Controls.Count - 1].Right;
            }
            AntdUI.Checkbox checkbox = new AntdUI.Checkbox()
            {
                AutoSize = true,
                AutoSizeMode = AntdUI.TAutoSize.Width,
                Checked = true,
                Location = new System.Drawing.Point(positionX, 12),
                Text = $"{moduleCode}{i}",
                Size = new System.Drawing.Size(72, 23)
            };
            checkbox.Click += ChkFilter_Click;
            pnlHoles.Controls.Add(checkbox);
        }
        pnlHoles.ResumeLayout(false);
        pnlHoles.PerformLayout();
    }

    private void InitMarkerCheckboxes()
    {
        pnlMarkers.SuspendLayout();
        pnlMarkers.Controls.Clear();
        foreach (var item in Constants.OrderlyPcrs)
        {
            int positionX = 10;
            if (pnlMarkers.Controls.Count > 0)
            {
                positionX = pnlMarkers.Controls[pnlMarkers.Controls.Count - 1].Right;
            }
            AntdUI.Checkbox checkbox = new AntdUI.Checkbox()
            {
                AutoSize = true,
                AutoSizeMode = AntdUI.TAutoSize.Width,
                Checked = false,
                Location = new System.Drawing.Point(positionX, 12),
                Text = item.Marker.Trim(),
                Size = new System.Drawing.Size(72, 23)
            };
            checkbox.Click += ChkFilter_Click;
            pnlMarkers.Controls.Add(checkbox);
        }
        pnlMarkers.ResumeLayout(false);
        pnlMarkers.PerformLayout();
    }

    /// <summary>
    /// 解析曲线LegendText,如LegendText = "A1|甲型流感病毒【FAM】",则提取["A1", "FAM"]
    /// </summary>
    /// <param name="legendText">曲线Legend文本</param>
    /// <returns>曲线包含的孔位、标记物信息</returns>
    private List<string> ParseLegendForFilters(string legendText)
    {
        var filters = new List<string>();
        if (!String.IsNullOrWhiteSpace(legendText) && legendText.Contains("|"))
        {
            var parts = legendText.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
            filters.Add(parts[0]); // 添加孔位,如:“A1”

            // 尝试从括号中提取标记物信息,如“FAM”
            var match = System.Text.RegularExpressions.Regex.Match(legendText, @"【(.*?)】");

            if (match.Success)
            {
                filters.Add(match.Groups[1].Value);
            }
        }
        return filters; // 返回 ["A1", "FAM"]
    }
}

}

Image
Dominant language
C#
Stars
6.8k
Forks
1k
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ScottPlot/ScottPlot

All issues in ScottPlot/ScottPlot

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.