fix: 그래프 생성 기능 수정

prototype
HyungJune Kim 10 months ago
parent 9a3ae0cda2
commit 6354aa82f7

@ -186,6 +186,18 @@
<RowDefinition/> <RowDefinition/>
<RowDefinition Height="50"/> <RowDefinition Height="50"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<Style x:Key="VisibleWhenTank" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedTab}" Value="Tank">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<StackPanel> <StackPanel>
<Grid Margin="15 15 15 10"> <Grid Margin="15 15 15 10">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -202,7 +214,7 @@
helper:ComboBoxHelper.SelectFirstOnItemsChange="True" helper:ComboBoxHelper.SelectFirstOnItemsChange="True"
IsEditable="False" IsTextSearchEnabled="False"/> IsEditable="False" IsTextSearchEnabled="False"/>
</Grid> </Grid>
<Grid Margin="15 0"> <Grid Margin="15 0 15 10">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/> <ColumnDefinition Width="80"/>
<ColumnDefinition/> <ColumnDefinition/>
@ -216,7 +228,7 @@
DisplayMemberPath="Display"/> DisplayMemberPath="Display"/>
</Grid> </Grid>
<StackPanel Margin="15 15 15 0"> <StackPanel Margin="15 0" Style="{StaticResource VisibleWhenTank}">
<TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center" <TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/> FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
@ -239,23 +251,6 @@
<Setter Property="Visibility" Value="Collapsed"/> <Setter Property="Visibility" Value="Collapsed"/>
</Style> </Style>
<!-- 보이기 토글용 스타일 --> <!-- 보이기 토글용 스타일 -->
<Style x:Key="VisibleWhenTrue" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STEP">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenTankNLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsTankAndLine}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}"> <Style x:Key="VisibleWhenLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE"> <DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
@ -308,23 +303,6 @@
DisplayMemberPath="Display" Margin="15 0 0 0" DisplayMemberPath="Display" Margin="15 0 0 0"
Height="40" Style="{StaticResource ComboBoxStyle}"/> Height="40" Style="{StaticResource ComboBoxStyle}"/>
</Grid> </Grid>
<!-- 옵션 -->
<StackPanel Orientation="Horizontal" Margin="0 15 0 0" Grid.Row="4">
<CheckBox Content="마커 표시" IsChecked="{Binding ShowMarkers}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<CheckBox Content="범례 표시" IsChecked="{Binding ShowLegends}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<!--<CheckBox Content="스무딩" IsChecked="{Binding UseSmoothing}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>-->
</StackPanel>
</StackPanel> </StackPanel>
<!--STEP--> <!--STEP-->
@ -354,7 +332,7 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid Margin="0 5 0 0"> <!--<Grid Margin="0 5 0 0">
<Grid.Style> <Grid.Style>
<Style TargetType="Grid"> <Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed"/> <Setter Property="Visibility" Value="Collapsed"/>
@ -376,10 +354,10 @@
Grid.Column="1" Grid.Column="1"
DisplayMemberPath="Display" Margin="15 0 0 0" DisplayMemberPath="Display" Margin="15 0 0 0"
Height="40" Style="{StaticResource ComboBoxStyle}"/> Height="40" Style="{StaticResource ComboBoxStyle}"/>
</Grid> </Grid>-->
<StackPanel> <StackPanel Margin="0 5 0 0">
<StackPanel.Style> <!--<StackPanel.Style>
<Style TargetType="StackPanel"> <Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/> <Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers> <Style.Triggers>
@ -388,7 +366,7 @@
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</StackPanel.Style> </StackPanel.Style>-->
<TextBlock Text="Y축 (복수 선택)" VerticalAlignment="Center" <TextBlock Text="Y축 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/> FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<!-- SelectedItems 바인딩을 위한 간단 Behavior는 아래 3) 참고 --> <!-- SelectedItems 바인딩을 위한 간단 Behavior는 아래 3) 참고 -->
@ -402,17 +380,6 @@
Style="{StaticResource MaterialDesignFilterChipListBox}"/> Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border> </Border>
</StackPanel> </StackPanel>
<!-- 옵션 -->
<StackPanel Orientation="Horizontal" Margin="0 15 0 0">
<CheckBox Content="마커 표시" IsChecked="{Binding ShowMarkers}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<CheckBox Content="범례 표시" IsChecked="{Binding ShowLegends}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
</StackPanel>
</StackPanel> </StackPanel>
<!-- SCATTER: 단일 Y + 옵션 --> <!-- SCATTER: 단일 Y + 옵션 -->
@ -445,12 +412,6 @@
Grid.Row="1" Grid.Column="2" VerticalAlignment="Center" Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/> FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
</Grid> </Grid>
<CheckBox Content="회귀선" IsChecked="{Binding ShowRegression}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="0" Grid.Row="1" Grid.Column="3"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
</StackPanel> </StackPanel>
<!-- BOX: 값 필드 + 그룹 필드 + 옵션 --> <!-- BOX: 값 필드 + 그룹 필드 + 옵션 -->
@ -508,6 +469,44 @@
</Grid> </Grid>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="15 0">
<CheckBox Content="범례 표시" IsChecked="{Binding ShowLegends}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="0 0 15 0" VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<CheckBox Content="마커 표시" IsChecked="{Binding ShowMarkers}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="0 0 15 0" VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource MaterialDesignUserForegroundCheckBox}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STEP">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
<CheckBox Content="회귀선" IsChecked="{Binding ShowRegression}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="0 0 15 0" VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource MaterialDesignUserForegroundCheckBox}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="SCATTER">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="15 0" Grid.Row="1" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal" Margin="15 0" Grid.Row="1" HorizontalAlignment="Right">
<Button Content="그래프 생성" Style="{StaticResource MaterialDesignFlatLightBgButton}" <Button Content="그래프 생성" Style="{StaticResource MaterialDesignFlatLightBgButton}"
FontWeight="Bold" Command="{Binding DrawGraphCommand}"/> FontWeight="Bold" Command="{Binding DrawGraphCommand}"/>

@ -1,4 +1,5 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Microsoft.VisualBasic;
using OxyPlot; using OxyPlot;
using OxyPlot.Axes; using OxyPlot.Axes;
using OxyPlot.Legends; using OxyPlot.Legends;
@ -24,26 +25,37 @@ namespace SmartAquaViewer.ViewModel
} }
public void SetTankLineGraph( public void SetTankLineGraph(
Dictionary<int, ObservableCollection<WaterQualityVO>> collection, ReadOnlyObservableCollection<WaterQualityVO> collection,
List<int>? selectedTankNums,
FieldItem? xField, FieldItem? yField, FieldItem? xField, FieldItem? yField,
bool isMarker, bool showLegends) bool isMarker, bool showLegends)
{ {
Model.Series.Clear(); Model.Series.Clear();
Model.Axes.Clear(); Model.Axes.Clear();
var xAxis = new DateTimeAxis bool xIsTime = string.Equals(xField!.Name, "RecordedTime", StringComparison.OrdinalIgnoreCase);
Axis xAxis = xIsTime
? new DateTimeAxis
{ {
Position = AxisPosition.Bottom, Position = AxisPosition.Bottom,
Title = "시간", Title = xField.Display,
StringFormat = "HH:mm:ss", StringFormat = "MM-dd\nHH:mm",
IntervalType = DateTimeIntervalType.Minutes, IntervalType = DateTimeIntervalType.Minutes,
MajorGridlineStyle = LineStyle.Solid, MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot MinorGridlineStyle = LineStyle.Dot
}
: new LinearAxis
{
Position = AxisPosition.Bottom,
Title = xField.Display,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
}; };
var yAxis = new LinearAxis var yAxis = new LinearAxis
{ {
Position = AxisPosition.Left, Position = AxisPosition.Left,
Title = yField.Display, Title = yField!.Display,
MajorGridlineStyle = LineStyle.Solid, MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot MinorGridlineStyle = LineStyle.Dot
}; };
@ -51,34 +63,57 @@ namespace SmartAquaViewer.ViewModel
Model.Axes.Add(xAxis); Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis); Model.Axes.Add(yAxis);
foreach (var (tankNum, datas) in collection.OrderBy(x => x.Key)) var allTankIds = collection.SelectMany(w => w.Tanks)
{ .Select(t => t.Number)
if (datas == null || datas.Count == 0) .Distinct()
continue; .OrderBy(id => id)
.ToList();
var tankIds = (selectedTankNums == null || !selectedTankNums.Any())
? allTankIds
: allTankIds.Where(id => selectedTankNums.Contains(id)).ToList();
if (tankIds.Count == 0) { Model.InvalidatePlot(true); return; }
var orderedCollection = collection.OrderBy(r => r.RecordedTime);
foreach (var tankId in tankIds)
{
var series = new LineSeries() var series = new LineSeries()
{ {
Title = tankNum.ToString(), Title = tankId.ToString(),
MarkerType = isMarker ? MarkerType.Circle : MarkerType.None, MarkerType = isMarker ? MarkerType.Circle : MarkerType.None,
MarkerSize = isMarker ? 3 : 0 MarkerSize = isMarker ? 3 : 0
}; };
foreach (var data in datas.OrderBy(d => d.RecordedTime)) var points = new List<DataPoint>();
foreach (var w in orderedCollection)
{ {
var tank = data.Tanks.Find(t => t.Number.Equals(tankNum)); var tank = w.Tanks.FirstOrDefault(t => t.Number == tankId);
var y = ResolveTank(tank, yField.Name!); if (tank is null) continue;
if (!y.HasValue) continue;
series.Points.Add(new DataPoint( double? xVal = xIsTime
DateTimeAxis.ToDouble(data.RecordedTime), // 여기서 recordedTime 사용 ? DateTimeAxis.ToDouble(w.RecordedTime)
y.Value)); : ResolveTank(tank, xField.Name!); // 리플렉션: double? 반환
double? yVal = ResolveTank(tank, yField.Name!);
// ⚠️ 둘 중 하나라도 없으면 스킵
if (!xVal.HasValue || !yVal.HasValue) continue;
points.Add(new DataPoint(xVal.Value, yVal.Value));
} }
foreach (var p in points.OrderBy(p => p.X))
series.Points.Add(p);
if (series.Points.Count > 0) if (series.Points.Count > 0)
{ {
var xTracker = xIsTime ? $"시간: {{2:HH:mm}}" : $"{xField.Display}: {{2:0.###}}";
// 트래커 포맷: 시간, 수조, 지표, 값 // 트래커 포맷: 시간, 수조, 지표, 값
series.TrackerFormatString = series.TrackerFormatString =
$"수조 {tankNum}\n시간: {{2:HH:mm}}\n{yField.Display}: {{4:0.###}}"; $"수조 {tankId}\n{xTracker}\n{yField.Display}: {{4:0.###}}";
Model.Series.Add(series); Model.Series.Add(series);
} }
} }
@ -173,13 +208,24 @@ namespace SmartAquaViewer.ViewModel
Model.Series.Clear(); Model.Series.Clear();
Model.Axes.Clear(); Model.Axes.Clear();
var xAxis = new DateTimeAxis bool xIsTime = string.Equals(xField!.Name, "RecordedTime", StringComparison.OrdinalIgnoreCase);
Axis xAxis = xIsTime
? new DateTimeAxis
{ {
Position = AxisPosition.Bottom, Position = AxisPosition.Bottom,
Title = xField.Display,
StringFormat = "HH:mm:ss", StringFormat = "HH:mm:ss",
IntervalType = DateTimeIntervalType.Minutes, IntervalType = DateTimeIntervalType.Minutes,
MajorGridlineStyle = LineStyle.Solid, MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot MinorGridlineStyle = LineStyle.Dot
}
: new LinearAxis
{
Position = AxisPosition.Bottom,
Title = xField.Display,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
}; };
var yAxis = new LinearAxis var yAxis = new LinearAxis
{ {
@ -198,8 +244,23 @@ namespace SmartAquaViewer.ViewModel
MarkerSize = isMarker ? 3 : 0 MarkerSize = isMarker ? 3 : 0
}; };
var points = new List<DataPoint>();
foreach (var r in collection.OrderBy(r => r.RecordedTime)) foreach (var r in collection.OrderBy(r => r.RecordedTime))
{ {
double? xVal = xIsTime
? DateTimeAxis.ToDouble(r.RecordedTime)
: double.NaN;
if (xVal.HasValue && double.IsNaN(xVal.Value))
{
xVal = selectedTab switch
{
MonitorTab.Filter => ResolveFilter(r, xField.Name!),
MonitorTab.Sterilizer => ResolveSterilizer(r, xField.Name!),
_ => xVal
};
}
double? y = null; double? y = null;
if (selectedTab.Equals(MonitorTab.Filter)) if (selectedTab.Equals(MonitorTab.Filter))
y = ResolveFilter(r, yField.Name!); y = ResolveFilter(r, yField.Name!);
@ -208,16 +269,21 @@ namespace SmartAquaViewer.ViewModel
if (!y.HasValue) continue; if (!y.HasValue) continue;
series.Points.Add(new DataPoint( points.Add(new DataPoint(
DateTimeAxis.ToDouble(r.RecordedTime), xVal!.Value,
y.Value)); y.Value));
}
foreach (var p in points.OrderBy(p => p.X))
series.Points.Add(p);
if (series.Points.Count > 0) if (series.Points.Count > 0)
{ {
var xTracker = xIsTime ? $"시간: {{2:HH:mm}}" : $"{xField.Display}: {{2:0.###}}";
// 트래커 포맷: 시간, 수조, 지표, 값 // 트래커 포맷: 시간, 수조, 지표, 값
series.TrackerFormatString = series.TrackerFormatString =
$"시간: {{2:HH:mm}}\n{yField.Display}: {{4:0.###}}"; $"{xTracker}\n{yField.Display}: {{4:0.###}}";
}
} }
Model.Series.Add(series); Model.Series.Add(series);
@ -330,21 +396,30 @@ namespace SmartAquaViewer.ViewModel
} }
public void SetScatterPlot( public void SetScatterPlot(
List<WaterQualityVO> rows, ReadOnlyObservableCollection<WaterQualityVO> collection,
FieldItem xAxisField, FieldItem xAxisField,
FieldItem yAxisField, FieldItem yAxisField,
List<int> selectedTankNums,
double markerSize = 3, double markerSize = 3,
bool showRegression = false) bool showRegression = false,
bool showLegends = true)
{ {
Model.Series.Clear(); Model.Series.Clear();
Model.Axes.Clear(); Model.Axes.Clear();
var xAxis = new LinearAxis bool xIsTime = string.Equals(xAxisField.Name, "RecordedTime", StringComparison.OrdinalIgnoreCase);
Axis xAxis = xIsTime
? new DateTimeAxis
{ {
Position = AxisPosition.Bottom, Position = AxisPosition.Bottom,
Title = xAxisField.Display, Title = xAxisField.Display,
MajorGridlineStyle = LineStyle.Solid, StringFormat = "MM-dd\nHH:mm"
MinorGridlineStyle = LineStyle.Dot }
: new LinearAxis
{
Position = AxisPosition.Bottom,
Title = xAxisField.Display
}; };
var yAxis = new LinearAxis var yAxis = new LinearAxis
{ {
@ -357,50 +432,110 @@ namespace SmartAquaViewer.ViewModel
Model.Axes.Add(xAxis); Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis); Model.Axes.Add(yAxis);
var scatterSeries = new ScatterSeries var allTankIds = collection.SelectMany(w => w.Tanks)
.Select(t => t.Number)
.Distinct()
.OrderBy(id => id)
.ToList();
var tankIds = (selectedTankNums == null || !selectedTankNums.Any())
? allTankIds
: allTankIds.Where(id => selectedTankNums.Contains(id)).ToList();
if (tankIds.Count == 0) { Model.InvalidatePlot(true); return; }
// 색상/마커
var colors = OxyPalettes.HueDistinct(tankIds.Count).Colors;
var markerCycle = new[]
{ {
MarkerType = MarkerType.Circle, MarkerType.Circle, MarkerType.Square, MarkerType.Triangle, MarkerType.Diamond,
MarkerType.Plus, MarkerType.Star, MarkerType.Cross
};
int k = 0;
foreach (var tankId in tankIds)
{
var series = new ScatterSeries
{
Title = $"Tank {tankId}",
MarkerType = markerCycle[k % markerCycle.Length],
MarkerSize = markerSize, MarkerSize = markerSize,
MarkerStroke = OxyColors.Black, // 색상 자동 배정에 맡기려면 MarkerFill 설정 생략해도 OK
MarkerFill = OxyColors.DeepSkyBlue MarkerFill = OxyColor.FromAColor(160, colors[k]), // 약간 투명
}; };
//foreach (var row in rows) // 포인트 수집 (시간순 정렬 권장)
//{ var points = new List<ScatterPoint>();
// double x = ResolveTank(row, xAxisField.Name) ?? double.NaN; foreach (var w in collection.OrderBy(r => r.RecordedTime))
// double y = ResolveTank(row, yAxisField.Name) ?? double.NaN; {
// scatterSeries.Points.Add(new ScatterPoint(x, y)); foreach (var t in w.Tanks)
//} {
if (t.Number != tankId) continue;
Model.Series.Add(scatterSeries); // X
double? xVal = xIsTime
? DateTimeAxis.ToDouble(w.RecordedTime)
: ResolveTank(t, xAxisField.Name!); // 수조 객체의 선택필드(리플렉션)
//if (showRegression && rows.Count > 1) // Y
//{ double? yVal = ResolveTank(t, yAxisField.Name!);
// var points = rows.Select(r
// => new DataPoint( if (xVal.HasValue && yVal.HasValue)
// ResolveTank(r, xAxisField.Name) ?? double.NaN, points.Add(new ScatterPoint(xVal.Value, yVal.Value));
// ResolveTank(r, yAxisField.Name) ?? double.NaN)) }
// .ToList(); }
// var regression = LinearRegression(points);
// 포인트 반영
// if (regression != null) if (points.Count > 0)
// { {
// var lineSeries = new LineSeries series.Points.AddRange(points);
// { series.TrackerFormatString =
// Title = "Regression", $"수조: {tankId}\nX: {{2:0.###}}\nY: {{4:0.###}}";
// Color = OxyColors.Red,
// StrokeThickness = 2 Model.Series.Add(series);
// };
// 옵션: 수조별 선형회귀선
// // 최소/최대 구간으로 선 그리기 if (showRegression && !xIsTime && points.Count >= 2)
// double minX = points.Min(p => p.X); {
// double maxX = points.Max(p => p.X); var (a, b) = FitLinear(points); // y = a*x + b
// lineSeries.Points.Add(new DataPoint(minX, regression.Value.Intercept + regression.Value.Slope * minX)); double minX = points.Min(p => p.X);
// lineSeries.Points.Add(new DataPoint(maxX, regression.Value.Intercept + regression.Value.Slope * maxX)); double maxX = points.Max(p => p.X);
// Model.Series.Add(lineSeries); var reg = new LineSeries
// } {
//} Title = $"Tank {tankId} 회귀",
StrokeThickness = 2,
Color = colors[k]
};
reg.Points.Add(new DataPoint(minX, a * minX + b));
reg.Points.Add(new DataPoint(maxX, a * maxX + b));
Model.Series.Add(reg);
}
}
k++;
}
Model.Legends.Clear();
Model.IsLegendVisible = showLegends;
Model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.RightTop,
LegendOrientation = LegendOrientation.Vertical,
LegendTitle = "수조",
TextColor = OxyColors.Black
});
Model.Legends.Clear();
Model.IsLegendVisible = showLegends;
Model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.RightTop,
LegendOrientation = LegendOrientation.Vertical,
LegendTitle = "수조",
TextColor = OxyColors.Black
});
Model.InvalidatePlot(true); Model.InvalidatePlot(true);
} }
@ -410,7 +545,7 @@ namespace SmartAquaViewer.ViewModel
MonitorTab selectedTab, MonitorTab selectedTab,
FieldItem xAxisField, FieldItem xAxisField,
ObservableCollection<FieldItem> yAxisFields, ObservableCollection<FieldItem> yAxisFields,
bool showMarker = false bool showMarker = false, bool showLegends = true
) )
{ {
Model.Series.Clear(); Model.Series.Clear();
@ -420,7 +555,8 @@ namespace SmartAquaViewer.ViewModel
{ {
Position = AxisPosition.Bottom, Position = AxisPosition.Bottom,
Title = "시간", Title = "시간",
GapWidth = 0.2 GapWidth = 0.2,
Angle = 45
}; };
foreach (var r in rows) foreach (var r in rows)
@ -462,21 +598,72 @@ namespace SmartAquaViewer.ViewModel
Model.InvalidatePlot(true); Model.InvalidatePlot(true);
} }
public void SetStatusSeriesStopPlot(List<WaterQualityVO> collection, public void SetStatusSeriesStopPlot(
FieldItem yAxisField, bool showMarker = false) List<WaterQualityVO> collection,
FieldItem yAxisField,
ObservableCollection<FieldItem> yAxisFields,
bool showMarker = false, bool showLegends = true)
{ {
Model.Series.Clear(); Model.Series.Clear();
Model.Axes.Clear(); Model.Axes.Clear();
var xAxis = new CategoryAxis
{
Position = AxisPosition.Bottom,
Title = "시간",
GapWidth = 0.2
};
foreach (var r in collection)
{
xAxis.Labels.Add(r.RecordedTime.ToString("HH:mm:ss"));
}
Model.Axes.Add(xAxis);
Model.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "값"
});
//var series = new StairStepSeries
//{
// MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
//};
//int i = 0;
//foreach (var r in collection.OrderBy(r => r.RecordedTime))
//{
// string? rawValue = ResolveStatus(r, yAxisField.Name);
// if (rawValue != null)
// {
// double y = MapDeviceStatus(rawValue);
// series.Points.Add(new DataPoint(i, y));
// }
// else
// {
// double? uvPower = ResolveUvPowerPerId(r, yAxisField.Name);
// if (uvPower.HasValue)
// {
// series.Points.Add(new DataPoint(i, uvPower.Value));
// }
// }
// i++;
//}
foreach (var field in yAxisFields)
{
var series = new StairStepSeries var series = new StairStepSeries
{ {
Title = field.Display,
MarkerType = showMarker ? MarkerType.Circle : MarkerType.None, MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
MarkerSize = 3
}; };
int i = 0; int i = 0;
foreach (var r in collection.OrderBy(r => r.RecordedTime)) foreach (var r in collection.OrderBy(r => r.RecordedTime))
{ {
string? rawValue = ResolveStatus(r, yAxisField.Name); string? rawValue = ResolveStatus(r, field.Name!);
if (rawValue != null) if (rawValue != null)
{ {
double y = MapDeviceStatus(rawValue); double y = MapDeviceStatus(rawValue);
@ -484,7 +671,7 @@ namespace SmartAquaViewer.ViewModel
} }
else else
{ {
double? uvPower = ResolveUvPowerPerId(r, yAxisField.Name); double? uvPower = ResolveUvPowerPerId(r, field.Name!);
if (uvPower.HasValue) if (uvPower.HasValue)
{ {
series.Points.Add(new DataPoint(i, uvPower.Value)); series.Points.Add(new DataPoint(i, uvPower.Value));
@ -494,6 +681,19 @@ namespace SmartAquaViewer.ViewModel
} }
Model.Series.Add(series); Model.Series.Add(series);
}
Model.Legends.Clear();
Model.IsLegendVisible = showLegends;
Model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.RightTop,
LegendOrientation = LegendOrientation.Vertical,
LegendTitle = "수조",
TextColor = OxyColors.Black
});
Model.InvalidatePlot(true); Model.InvalidatePlot(true);
} }
@ -843,6 +1043,18 @@ namespace SmartAquaViewer.ViewModel
return (slope, intercept); return (slope, intercept);
} }
private (double a, double b) FitLinear(IEnumerable<ScatterPoint> pts)
{
int n = 0;
double sumX = 0, sumY = 0, sumXX = 0, sumXY = 0;
foreach (var p in pts) { n++; sumX += p.X; sumY += p.Y; sumXX += p.X * p.X; sumXY += p.X * p.Y; }
double denom = n * sumXX - sumX * sumX;
if (n < 2 || Math.Abs(denom) < 1e-12) return (0, pts.First().Y); // 수직/특이 케이스
double a = (n * sumXY - sumX * sumY) / denom;
double b = (sumY - a * sumX) / n;
return (a, b);
}
private double MapDeviceStatus(string status) private double MapDeviceStatus(string status)
{ {
return status switch return status switch

@ -325,11 +325,7 @@ namespace SmartAquaViewer.ViewModel
SetGraphData_Box_Tank(); SetGraphData_Box_Tank();
break; break;
case GraphType.SCATTER: case GraphType.SCATTER:
var xFieldScatter = SelectedXField; SetGraphData_Scatter_Tank();
var yFiledScatter = SelectedYField;
var markerSIze = ScatterMarkerSize;
var showRegression = ShowRegression;
GraphControlVM.SetScatterPlot(WaterQualityList.ToList(), xFieldScatter, yFiledScatter, markerSIze, showRegression);
break; break;
case GraphType.STEP: case GraphType.STEP:
var xFieldStep = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null; var xFieldStep = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null;
@ -337,9 +333,9 @@ namespace SmartAquaViewer.ViewModel
var yFiledStep = SelectedYField; var yFiledStep = SelectedYField;
var showMarkerStep = ShowMarkers; var showMarkerStep = ShowMarkers;
if (SelectedKind.Equals(StepFieldKind.Status)) if (SelectedKind.Equals(StepFieldKind.Status))
GraphControlVM.SetStatusSeriesStopPlot(WaterQualityList.ToList(), yFiledStep, showMarkerStep); GraphControlVM.SetStatusSeriesStopPlot(WaterQualityList.ToList(), SelectedYField, SelectedYFields, ShowMarkers, ShowLegends);
else else
GraphControlVM.SetStepPlot(WaterQualityList.ToList(), SelectedTab, xFieldStep, tFieldsStep, showMarkerStep); GraphControlVM.SetStepPlot(WaterQualityList.ToList(), SelectedTab, xFieldStep, SelectedYFields, ShowMarkers, ShowLegends);
break; break;
default: default:
break; break;
@ -363,7 +359,7 @@ namespace SmartAquaViewer.ViewModel
g.Select(x => x.VO).OrderBy(vo => vo.RecordedTime)) g.Select(x => x.VO).OrderBy(vo => vo.RecordedTime))
); );
GraphControlVM.SetTankLineGraph(selectedTanks, SelectedXField, SelectedYField, ShowMarkers, ShowLegends); GraphControlVM.SetTankLineGraph(WaterQualityList, keys, SelectedXField, SelectedYField, ShowMarkers, ShowLegends);
} }
private void SetGraphData_Box_Tank() private void SetGraphData_Box_Tank()
@ -378,6 +374,16 @@ namespace SmartAquaViewer.ViewModel
GraphControlVM.SetBoxPlot(WaterQualityList, keys, SelectedXField, SelectedYField, BoxWidth, boxTimeSpan); GraphControlVM.SetBoxPlot(WaterQualityList, keys, SelectedXField, SelectedYField, BoxWidth, boxTimeSpan);
} }
private void SetGraphData_Scatter_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (SelectedYField == null) return;
var keys = SelectedWaterTanks.Keys.ToList();
GraphControlVM.SetScatterPlot(WaterQualityList, SelectedXField, SelectedYField, keys, ScatterMarkerSize, ShowRegression, ShowLegends);
}
private void SetGraphType() private void SetGraphType()
{ {
GraphTypes.Clear(); GraphTypes.Clear();
@ -491,6 +497,8 @@ namespace SmartAquaViewer.ViewModel
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime)) SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault(); ?? AvailableFields.FirstOrDefault();
if (SelectedGraphType != GraphType.STEP) SelectedKind = StepFieldKind.Sensor;
IEnumerable<FieldItem> src = AvailableFields.Where(f => f.Kind == SelectedKind); IEnumerable<FieldItem> src = AvailableFields.Where(f => f.Kind == SelectedKind);
if (SelectedGraphType is GraphType.LINE or GraphType.SCATTER or GraphType.BOX) if (SelectedGraphType is GraphType.LINE or GraphType.SCATTER or GraphType.BOX)
@ -520,8 +528,6 @@ namespace SmartAquaViewer.ViewModel
UseSmoothing = false; UseSmoothing = false;
break; break;
case GraphType.STEP: case GraphType.STEP:
var def = YFieldCandidates.FirstOrDefault();
if (def != null) SelectedYFields.Add(def);
ShowMarkers = false; ShowMarkers = false;
UseSmoothing = false; UseSmoothing = false;
break; break;

Loading…
Cancel
Save