|
|
|
|
@ -70,7 +70,7 @@ namespace SmartAquaViewer.ViewModel
|
|
|
|
|
|
|
|
|
|
foreach (var r in data.OrderBy(r => r.RecordedTime))
|
|
|
|
|
{
|
|
|
|
|
var y = ResolveTankY(r, yField.Name!);
|
|
|
|
|
var y = ResolveTank(r, yField.Name!);
|
|
|
|
|
if (!y.HasValue) continue;
|
|
|
|
|
|
|
|
|
|
series.Points.Add(new DataPoint(
|
|
|
|
|
@ -128,9 +128,9 @@ namespace SmartAquaViewer.ViewModel
|
|
|
|
|
{
|
|
|
|
|
double? y = null;
|
|
|
|
|
if (selectedTab.Equals(MonitorTab.Filter))
|
|
|
|
|
y = ResolveFilterY(r, yField.Name!);
|
|
|
|
|
y = ResolveFilter(r, yField.Name!);
|
|
|
|
|
else if (selectedTab.Equals(MonitorTab.Sterilizer))
|
|
|
|
|
y = ResolveSterilizerY(r, yField.Name!);
|
|
|
|
|
y = ResolveSterilizer(r, yField.Name!);
|
|
|
|
|
|
|
|
|
|
if (!y.HasValue) continue;
|
|
|
|
|
|
|
|
|
|
@ -191,14 +191,14 @@ namespace SmartAquaViewer.ViewModel
|
|
|
|
|
|
|
|
|
|
var col = rows
|
|
|
|
|
.OrderBy(r => r.RecordedTime)
|
|
|
|
|
.GroupBy(g => ResolveTankX(g, xAxisKind.Name, bucket))
|
|
|
|
|
.GroupBy(g => ResolveTankOrTime(g, xAxisKind.Name, bucket))
|
|
|
|
|
.ToDictionary(g => g.Key, g => g.ToList());
|
|
|
|
|
|
|
|
|
|
foreach(var (key, data) in col.OrderBy(x => x.Key))
|
|
|
|
|
{
|
|
|
|
|
// 1) 수조/시간별 값 리스트
|
|
|
|
|
var values = data
|
|
|
|
|
.Select(d => ResolveTankY(d, valueField.Name!))
|
|
|
|
|
.Select(d => ResolveTank(d, valueField.Name!))
|
|
|
|
|
.OfType<double>() // object→double
|
|
|
|
|
.Where(v => !double.IsNaN(v) && !double.IsInfinity(v))
|
|
|
|
|
.OrderBy(v => v)
|
|
|
|
|
@ -234,71 +234,99 @@ namespace SmartAquaViewer.ViewModel
|
|
|
|
|
series.Items.Add(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//// [X] = 값 리스트 (그룹 없음)
|
|
|
|
|
//var byX = new Dictionary<string, List<double>>();
|
|
|
|
|
|
|
|
|
|
//// byGroup[gKey][xKey] = 값 리스트
|
|
|
|
|
|
|
|
|
|
//foreach (var r in rows)
|
|
|
|
|
//{
|
|
|
|
|
// var val = (double)ResolveTankY(r, valueField.Name);
|
|
|
|
|
// if (double.IsNaN(val) || double.IsInfinity(val)) continue;
|
|
|
|
|
|
|
|
|
|
// string xKey = ResolveTankX(r, xAxisKind.Name, bucket);
|
|
|
|
|
// if (!byX.TryGetValue(xKey, out var list))
|
|
|
|
|
// byX[xKey] = list = new List<double>();
|
|
|
|
|
Model.Series.Add(series);
|
|
|
|
|
Model.IsLegendVisible = false;
|
|
|
|
|
Model.InvalidatePlot(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// list.Add(val);
|
|
|
|
|
//}
|
|
|
|
|
public void SetScatterPlot(
|
|
|
|
|
List<WaterQualityVO> rows,
|
|
|
|
|
FieldItem xAxisField,
|
|
|
|
|
FieldItem yAxisField,
|
|
|
|
|
double markerSize = 3,
|
|
|
|
|
bool showRegression = false)
|
|
|
|
|
{
|
|
|
|
|
Model.Series.Clear();
|
|
|
|
|
Model.Axes.Clear();
|
|
|
|
|
|
|
|
|
|
//// X축 라벨(정렬) + 너무 많으면 최근 N개만
|
|
|
|
|
//var xKeys = byX.Keys.OrderBy(k => k, new KeyComparer()).ToList();
|
|
|
|
|
//if (xKeys.Count == 0) { Model.InvalidatePlot(true); return; }
|
|
|
|
|
//if (xKeys.Count > maxCategories)
|
|
|
|
|
// xKeys = xKeys.Skip(xKeys.Count - maxCategories).ToList();
|
|
|
|
|
var xAxis = new LinearAxis
|
|
|
|
|
{
|
|
|
|
|
Position = AxisPosition.Bottom,
|
|
|
|
|
Title = xAxisField.Display,
|
|
|
|
|
MajorGridlineStyle = LineStyle.Solid,
|
|
|
|
|
MinorGridlineStyle = LineStyle.Dot
|
|
|
|
|
};
|
|
|
|
|
var yAxis = new LinearAxis
|
|
|
|
|
{
|
|
|
|
|
Position = AxisPosition.Left,
|
|
|
|
|
Title = yAxisField.Display,
|
|
|
|
|
MajorGridlineStyle = LineStyle.Solid,
|
|
|
|
|
MinorGridlineStyle = LineStyle.Dot
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//xAxis.Labels.Clear();
|
|
|
|
|
//xAxis.Labels.AddRange(xKeys);
|
|
|
|
|
Model.Axes.Add(xAxis);
|
|
|
|
|
Model.Axes.Add(yAxis);
|
|
|
|
|
|
|
|
|
|
//var series = new BoxPlotSeries { BoxWidth = boxWidth };
|
|
|
|
|
var scatterSeries = new ScatterSeries
|
|
|
|
|
{
|
|
|
|
|
MarkerType = MarkerType.Circle,
|
|
|
|
|
MarkerSize = markerSize,
|
|
|
|
|
MarkerStroke = OxyColors.Black,
|
|
|
|
|
MarkerFill = OxyColors.DeepSkyBlue
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//for (int xi = 0; xi < xKeys.Count; xi++)
|
|
|
|
|
//{
|
|
|
|
|
// var list = byX[xKeys[xi]];
|
|
|
|
|
// if (list == null || list.Count == 0) continue;
|
|
|
|
|
foreach (var row in rows)
|
|
|
|
|
{
|
|
|
|
|
double x = ResolveTank(row, xAxisField.Name) ?? double.NaN;
|
|
|
|
|
double y = ResolveTank(row, yAxisField.Name) ?? double.NaN;
|
|
|
|
|
scatterSeries.Points.Add(new ScatterPoint(x, y));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// list.Sort();
|
|
|
|
|
Model.Series.Add(scatterSeries);
|
|
|
|
|
|
|
|
|
|
// // 표본 1개도 표시(퇴화 박스)
|
|
|
|
|
// if (list.Count == 1)
|
|
|
|
|
// {
|
|
|
|
|
// double v = list[0];
|
|
|
|
|
// series.Items.Add(new BoxPlotItem(xi, v, v, v, v, v));
|
|
|
|
|
// continue;
|
|
|
|
|
// }
|
|
|
|
|
if (showRegression && rows.Count > 1)
|
|
|
|
|
{
|
|
|
|
|
var points = rows.Select(r
|
|
|
|
|
=> new DataPoint(
|
|
|
|
|
ResolveTank(r, xAxisField.Name) ?? double.NaN,
|
|
|
|
|
ResolveTank(r, yAxisField.Name) ?? double.NaN))
|
|
|
|
|
.ToList();
|
|
|
|
|
var regression = LinearRegression(points);
|
|
|
|
|
|
|
|
|
|
// var (lw, q1, med, q3, uw, outs) = Summarize(list);
|
|
|
|
|
// var item = new BoxPlotItem(xi, lw, q1, med, q3, uw);
|
|
|
|
|
// foreach (var o in outs) item.Outliers.Add(o);
|
|
|
|
|
// series.Items.Add(item);
|
|
|
|
|
//}
|
|
|
|
|
if (regression != null)
|
|
|
|
|
{
|
|
|
|
|
var lineSeries = new LineSeries
|
|
|
|
|
{
|
|
|
|
|
Title = "Regression",
|
|
|
|
|
Color = OxyColors.Red,
|
|
|
|
|
StrokeThickness = 2
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 최소/최대 구간으로 선 그리기
|
|
|
|
|
double minX = points.Min(p => p.X);
|
|
|
|
|
double maxX = points.Max(p => p.X);
|
|
|
|
|
lineSeries.Points.Add(new DataPoint(minX, regression.Value.Intercept + regression.Value.Slope * minX));
|
|
|
|
|
lineSeries.Points.Add(new DataPoint(maxX, regression.Value.Intercept + regression.Value.Slope * maxX));
|
|
|
|
|
|
|
|
|
|
Model.Series.Add(lineSeries);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Model.Series.Add(series);
|
|
|
|
|
Model.IsLegendVisible = false;
|
|
|
|
|
Model.InvalidatePlot(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static DateTime FloorToBucket(DateTime dt, TimeSpan bucket)
|
|
|
|
|
private DateTime FloorToBucket(DateTime dt, TimeSpan bucket)
|
|
|
|
|
{
|
|
|
|
|
long ticks = bucket.Ticks;
|
|
|
|
|
long floored = dt.Ticks - (dt.Ticks % ticks);
|
|
|
|
|
return new DateTime(floored, dt.Kind);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private double? ResolveTankY(WaterQualityVO vo, string fieldName)
|
|
|
|
|
private double? ResolveTank(WaterQualityVO vo, string fieldName)
|
|
|
|
|
{
|
|
|
|
|
return fieldName switch
|
|
|
|
|
{
|
|
|
|
|
"RecordedTime" => DateTimeAxis.ToDouble(vo.RecordedTime),
|
|
|
|
|
"Tank.DOValue" => vo.Tank.DOValue,
|
|
|
|
|
"Tank.PH" => vo.Tank.PH,
|
|
|
|
|
"Tank.ORP" => vo.Tank.ORP,
|
|
|
|
|
@ -308,7 +336,7 @@ namespace SmartAquaViewer.ViewModel
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private double? ResolveFilterY(WaterQualityVO vo, string fieldName)
|
|
|
|
|
private double? ResolveFilter(WaterQualityVO vo, string fieldName)
|
|
|
|
|
{
|
|
|
|
|
return fieldName switch
|
|
|
|
|
{
|
|
|
|
|
@ -323,7 +351,7 @@ namespace SmartAquaViewer.ViewModel
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private double? ResolveSterilizerY(WaterQualityVO vo, string fieldName)
|
|
|
|
|
private double? ResolveSterilizer(WaterQualityVO vo, string fieldName)
|
|
|
|
|
{
|
|
|
|
|
return fieldName switch
|
|
|
|
|
{
|
|
|
|
|
@ -332,7 +360,7 @@ namespace SmartAquaViewer.ViewModel
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string ResolveTankX(WaterQualityVO vo, string fieldName, TimeSpan? bucket = null)
|
|
|
|
|
private string ResolveTankOrTime(WaterQualityVO vo, string fieldName, TimeSpan? bucket = null)
|
|
|
|
|
{
|
|
|
|
|
return fieldName switch
|
|
|
|
|
{
|
|
|
|
|
@ -390,6 +418,26 @@ namespace SmartAquaViewer.ViewModel
|
|
|
|
|
return (lw, q1, med, q3, uw, outs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 단순 선형회귀 계산
|
|
|
|
|
private (double Slope, double Intercept)? LinearRegression(List<DataPoint> points)
|
|
|
|
|
{
|
|
|
|
|
int n = points.Count;
|
|
|
|
|
if (n < 2) return null;
|
|
|
|
|
|
|
|
|
|
double avgX = points.Average(p => p.X);
|
|
|
|
|
double avgY = points.Average(p => p.Y);
|
|
|
|
|
|
|
|
|
|
double numerator = points.Sum(p => (p.X - avgX) * (p.Y - avgY));
|
|
|
|
|
double denominator = points.Sum(p => Math.Pow(p.X - avgX, 2));
|
|
|
|
|
|
|
|
|
|
if (denominator == 0) return null;
|
|
|
|
|
|
|
|
|
|
double slope = numerator / denominator;
|
|
|
|
|
double intercept = avgY - slope * avgX;
|
|
|
|
|
|
|
|
|
|
return (slope, intercept);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class KeyComparer : IComparer<string>
|
|
|
|
|
{
|
|
|
|
|
public int Compare(string? a, string? b)
|
|
|
|
|
|