There is a bug in the "DataLogger Add Marker" feature?
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
- Domain
- data-visualization, desktop
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"]
}
}
}
- Dominant language
- C#
- Stars
- 6.8k
- Forks
- 1k
- PR merge metrics
- No merged PRs in 30d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ScottPlot/ScottPlot
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
All issues in ScottPlot/ScottPlot
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SubtitleEdit/subtitleedit#15108 · 1 comment ·
-
area/docs-content Bug pulumi/docs
Difficulty 1/5 1-3 hours Newbie friendliness 94/100
-
agentic-workflows untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 76/100