diff --git a/SmartAquaViewer/View/MonitoringView.xaml b/SmartAquaViewer/View/MonitoringView.xaml
index 5ecb1ef..9b055a4 100644
--- a/SmartAquaViewer/View/MonitoringView.xaml
+++ b/SmartAquaViewer/View/MonitoringView.xaml
@@ -315,56 +315,46 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -382,7 +372,21 @@
FontSize="18" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
-
+
+
+
+
+
+
+
+
+
+
diff --git a/SmartAquaViewer/ViewModel/GraphControlViewModel.cs b/SmartAquaViewer/ViewModel/GraphControlViewModel.cs
index 0acbb4d..87f327c 100644
--- a/SmartAquaViewer/ViewModel/GraphControlViewModel.cs
+++ b/SmartAquaViewer/ViewModel/GraphControlViewModel.cs
@@ -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() // 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>();
-
- //// 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();
+ Model.Series.Add(series);
+ Model.IsLegendVisible = false;
+ Model.InvalidatePlot(true);
+ }
- // list.Add(val);
- //}
+ public void SetScatterPlot(
+ List 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 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
{
public int Compare(string? a, string? b)
diff --git a/SmartAquaViewer/ViewModel/MonitoringViewModel.cs b/SmartAquaViewer/ViewModel/MonitoringViewModel.cs
index 54e53d6..deefae8 100644
--- a/SmartAquaViewer/ViewModel/MonitoringViewModel.cs
+++ b/SmartAquaViewer/ViewModel/MonitoringViewModel.cs
@@ -255,7 +255,7 @@ namespace SmartAquaViewer.ViewModel
var xField = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null;
var yField = SelectedYField;
var isMarker = ShowMarkers;
- if (SelectedTab.Equals(MonitorTab.Tank)) SetGraphData_Line_Tank(SelectedWaterTanks, xField, yField, isMarker);
+ if (SelectedTab.Equals(MonitorTab.Tank)) SetGraphData_Line_Tank(xField, yField, isMarker);
else GraphControlVM.SetDefaultLineGraph(WaterQualityList, SelectedTab, xField, yField, isMarker);
break;
case GraphType.BOX:
@@ -266,25 +266,24 @@ namespace SmartAquaViewer.ViewModel
GraphControlVM.SetBoxPlot(WaterQualityList, xFieldBox, dataFieldBox, boxWidth, boxTimeSpan);
break;
case GraphType.SCATTER:
- var xFieldScatter = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null;
+ var xFieldScatter = SelectedXField;
var yFiledScatter = SelectedYField;
var markerSIze = ScatterMarkerSize;
var showRegression = ShowRegression;
+ GraphControlVM.SetScatterPlot(WaterQualityList, xFieldScatter, yFiledScatter, markerSIze, showRegression);
break;
default:
break;
}
}
- private void SetGraphData_Line_Tank(object data, FieldItem? xField, FieldItem? yField, bool showMarkers)
+ private void SetGraphData_Line_Tank(FieldItem? xField, FieldItem? yField, bool showMarkers)
{
if (SelectedTab != MonitorTab.Tank) return;
if (xField?.Name != "RecordedTime" || yField == null) return;
if (SelectedWaterTanks.Count == 0) return;
- var collection = data as Dictionary>;
-
- GraphControlVM.SetTankLineGraph(collection, xField, yField, showMarkers);
+ GraphControlVM.SetTankLineGraph(SelectedWaterTanks, xField, yField, showMarkers);
}
private void SetGraphType()
@@ -357,6 +356,7 @@ namespace SmartAquaViewer.ViewModel
// X축: 시간 우선
foreach (var f in AvailableFields)
{
+ if (SelectedGraphType == GraphType.SCATTER && f.Name.Equals("Tank.Number")) continue;
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.LINE || SelectedGraphType == GraphType.STEP) break;
if (SelectedGraphType == GraphType.BOX && f.Name.Equals("Tank.Number")) break;