feat: Step 그래프 생성 기능 추가

hhsung_work
HyungJune Kim 10 months ago
parent fecca36925
commit e794514592

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace SmartAquaViewer.Classes
{
public class EnumEqualsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value != null && parameter != null && value.Equals(parameter);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> (value is bool b && b) ? parameter! : Binding.DoNothing;
}
public class BoolToPowerConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is bool b && b
? "On"
: "Off";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value as string;
}
}
}

@ -13,7 +13,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
using static SmartAquaViewer.Model.Enums; using SmartAquaViewer.Model;
namespace SmartAquaViewer.Controls namespace SmartAquaViewer.Controls
{ {

@ -6,21 +6,30 @@ using System.Threading.Tasks;
namespace SmartAquaViewer.Model namespace SmartAquaViewer.Model
{ {
public class Enums public enum MonitorTab
{ {
public enum MonitorTab Tank,
{ Filter,
Tank, Sterilizer
Filter, }
Sterilizer
} public enum GraphType
{
LINE,
BOX,
SCATTER,
STEP
}
public enum GraphType public enum StepFieldKind
{ {
LINE, Status, // 전원/상태
BOX, Sensor // 센서 값
SCATTER, }
STEP
} public enum PowerStatus
{
Off,
On
} }
} }

@ -6,9 +6,17 @@
xmlns:local="clr-namespace:SmartAquaViewer.View" xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:control="clr-namespace:SmartAquaViewer.Controls" xmlns:control="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper" xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:model="clr-namespace:SmartAquaViewer.Model"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="940" d:DesignWidth="1650"> d:DesignHeight="940" d:DesignWidth="1650">
<UserControl.Resources>
<classes:EnumEqualsConverter x:Key="EnumEqualsConverter"/>
<classes:BoolToPowerConverter x:Key="BoolToPowerConverter"/>
</UserControl.Resources>
<Border> <Border>
<md:DrawerHost BottomDrawerBackground="#122136" IsBottomDrawerOpen="{Binding IsOpenMode}" OpenMode="Standard"> <md:DrawerHost BottomDrawerBackground="#122136" IsBottomDrawerOpen="{Binding IsOpenMode}" OpenMode="Standard">
<Grid Background="#243851"> <Grid Background="#243851">
@ -83,7 +91,7 @@
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}" Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="모래여과기 전원" Binding="{Binding Filtering.SandFilterPower}" <DataGridTextColumn Header="모래여과기 전원" Binding="{Binding Filtering.SandFilterPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 pH" Binding="{Binding Filtering.SumpPH}" <DataGridTextColumn Header="섬프탱크 pH" Binding="{Binding Filtering.SumpPH}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
@ -95,17 +103,17 @@
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 수온(°C)" Binding="{Binding Filtering.SumpTemperature, StringFormat=\{0:F2\}}" <DataGridTextColumn Header="섬프탱크 수온(°C)" Binding="{Binding Filtering.SumpTemperature, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="순환펌프 전원" Binding="{Binding Filtering.CirculationPumpPower}" <DataGridTextColumn Header="순환펌프 전원" Binding="{Binding Filtering.CirculationPumpPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="인버터 제어기 상태" Binding="{Binding Filtering.InverterControllerStatus}" <DataGridTextColumn Header="인버터 제어기 상태" Binding="{Binding Filtering.InverterControllerStatus}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="순환펌프 유량(m³/s)" Binding="{Binding Filtering.FlowRate, StringFormat=\{0:F2\}}" <DataGridTextColumn Header="순환펌프 유량(m³/s)" Binding="{Binding Filtering.FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="히트펌프 전원" Binding="{Binding Filtering.HeatPumpPower}" <DataGridTextColumn Header="히트펌프 전원" Binding="{Binding Filtering.HeatPumpPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="히트펌프 온도(°C)" Binding="{Binding Filtering.HeatPumpTemperature, StringFormat=\{0:F2\}}" <DataGridTextColumn Header="히트펌프 온도(°C)" Binding="{Binding Filtering.HeatPumpTemperature, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="에어브로와 전원" Binding="{Binding Filtering.AirBlowerPower}" <DataGridTextColumn Header="에어브로와 전원" Binding="{Binding Filtering.AirBlowerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
@ -122,17 +130,17 @@
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}" Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존 발생기 전원" Binding="{Binding Sterilizing.OzoneGeneratorPower}" <DataGridTextColumn Header="오존 발생기 전원" Binding="{Binding Sterilizing.OzoneGeneratorPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 ID" Binding="{Binding Sterilizing.UVSterilizerId}" <DataGridTextColumn Header="자외선 살균기 ID" Binding="{Binding Sterilizing.UVSterilizerId}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 전원" Binding="{Binding Sterilizing.UVSterilizerPower}" <DataGridTextColumn Header="자외선 살균기 전원" Binding="{Binding Sterilizing.UVSterilizerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 전원" Binding="{Binding Sterilizing.OzoneDissolverPower}" <DataGridTextColumn Header="오존용해장치 전원" Binding="{Binding Sterilizing.OzoneDissolverPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 압력(kPa)" Binding="{Binding Sterilizing.OzoneDissolverPressure, StringFormat=\{0:F1\}}" <DataGridTextColumn Header="오존용해장치 압력(kPa)" Binding="{Binding Sterilizing.OzoneDissolverPressure, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="배오존장치 전원" Binding="{Binding Sterilizing.ExcessOzoneDestroyerPower}" <DataGridTextColumn Header="배오존장치 전원" Binding="{Binding Sterilizing.ExcessOzoneDestroyerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
@ -254,8 +262,8 @@
</Style> </Style>
</Grid.Resources> </Grid.Resources>
<!-- LINE/STEP: 다중 선택 --> <!-- LINE -->
<StackPanel Style="{StaticResource VisibleWhenTrue}"> <StackPanel Style="{StaticResource VisibleWhenLine}">
<StackPanel Style="{StaticResource VisibleWhenTankNLine}"> <StackPanel Style="{StaticResource VisibleWhenTankNLine}">
<TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center" <TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/> FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
@ -287,7 +295,81 @@
Height="40" Style="{StaticResource ComboBoxStyle}"/> Height="40" Style="{StaticResource ComboBoxStyle}"/>
</Grid> </Grid>
<StackPanel Style="{StaticResource VisibleWhenStep}"> <!-- 옵션 -->
<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 UseSmoothing}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>-->
</StackPanel>
</StackPanel>
<!--STEP-->
<StackPanel Style="{StaticResource VisibleWhenStep}">
<Grid Margin="0 5 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="필드 종류" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Center">
<RadioButton x:Name="rbStatus" Content="전원/상태"
GroupName="strpPlot" Margin="0 0 30 0"
Foreground="White" FontSize="20"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding SelectedKind, Mode=TwoWay,
Converter={StaticResource EnumEqualsConverter},
ConverterParameter={x:Static model:StepFieldKind.Status}}"/>
<RadioButton x:Name="rbValue" Content="센서 값"
GroupName="strpPlot" Grid.Column="1"
Foreground="White" FontSize="20"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding SelectedKind, Mode=TwoWay,
Converter={StaticResource EnumEqualsConverter},
ConverterParameter={x:Static model:StepFieldKind.Sensor}}"/>
</StackPanel>
</Grid>
<Grid Margin="0 5 0 0">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=rbStatus}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField, Mode=TwoWay}"
Grid.Column="1"
DisplayMemberPath="Display" Margin="15 0 0 0"
Height="40" Style="{StaticResource ComboBoxStyle}"/>
</Grid>
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=rbValue}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</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) 참고 -->
@ -297,7 +379,7 @@
SelectionMode="Extended" SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}" helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
Height="Auto" Background="White" Height="Auto" Background="White"
FontSize="16" FontWeight="Bold" FontSize="14" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/> Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border> </Border>
</StackPanel> </StackPanel>
@ -307,10 +389,6 @@
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White" FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/> Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<!--<CheckBox Content="스무딩" IsChecked="{Binding UseSmoothing}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>-->
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

@ -18,7 +18,7 @@ using SmartAquaViewer.Classes;
using SmartAquaViewer.DataAnalysis; using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model; using SmartAquaViewer.Model;
using SmartAquaViewer.ViewModel; using SmartAquaViewer.ViewModel;
using static SmartAquaViewer.Model.Enums; using SmartAquaViewer.Model;
namespace SmartAquaViewer.View namespace SmartAquaViewer.View
{ {

@ -11,7 +11,7 @@ using OxyPlot.Axes;
using OxyPlot.Legends; using OxyPlot.Legends;
using OxyPlot.Series; using OxyPlot.Series;
using SmartAquaViewer.DataAnalysis; using SmartAquaViewer.DataAnalysis;
using static SmartAquaViewer.Model.Enums; using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel namespace SmartAquaViewer.ViewModel
{ {
@ -315,6 +315,98 @@ namespace SmartAquaViewer.ViewModel
Model.InvalidatePlot(true); Model.InvalidatePlot(true);
} }
public void SetStepPlot(
List<WaterQualityVO> rows,
MonitorTab selectedTab,
FieldItem xAxisField,
ObservableCollection<FieldItem> yAxisFields,
bool showMarker = false
)
{
Model.Series.Clear();
Model.Axes.Clear();
var xAxis = new CategoryAxis
{
Position = AxisPosition.Bottom,
Title = "시간",
GapWidth = 0.2
};
foreach (var r in rows)
{
xAxis.Labels.Add(r.RecordedTime.ToString("HH:mm:ss"));
}
Model.Axes.Add(xAxis);
Model.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "값"
});
foreach (var field in yAxisFields)
{
var series = new StairStepSeries
{
Title = field.Display,
MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
MarkerSize = 3
};
int i = 0;
foreach (var r in rows.OrderBy(r => r.RecordedTime))
{
double? y = selectedTab.Equals(MonitorTab.Filter)
? ResolveFilter(r, field.Name!)
: ResolveSterilizer(r, field.Name!);
if (y.HasValue)
series.Points.Add(new DataPoint(i, y.Value));
i++;
}
Model.Series.Add(series);
}
Model.InvalidatePlot(true);
}
public void SetStatusSeriesStopPlot(List<WaterQualityVO> rows,
FieldItem yAxisField, bool showMarker = false)
{
Model.Series.Clear();
Model.Axes.Clear();
var series = new StairStepSeries
{
MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
};
int i = 0;
foreach (var r in rows.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++;
}
Model.Series.Add(series);
Model.InvalidatePlot(true);
}
private DateTime FloorToBucket(DateTime dt, TimeSpan bucket) private DateTime FloorToBucket(DateTime dt, TimeSpan bucket)
{ {
long ticks = bucket.Ticks; long ticks = bucket.Ticks;
@ -360,6 +452,22 @@ namespace SmartAquaViewer.ViewModel
}; };
} }
private string? ResolveStatus(WaterQualityVO vo, string field)
{
return field switch
{
"Filtering.SandFilterPower" => vo.Filtering.SandFilterPower.ToString(),
"Filtering.CirculationPumpPower" => vo.Filtering.CirculationPumpPower.ToString(),
"Filtering.InverterControllerStatus" => vo.Filtering.InverterControllerStatus,
"Filtering.HeatPumpPower" => vo.Filtering.HeatPumpPower.ToString(),
"Filtering.AirBlowerPower" => vo.Filtering.AirBlowerPower.ToString(),
"Sterilizing.OzoneGeneratorPower" => vo.Sterilizing.OzoneGeneratorPower.ToString(),
"Sterilizing.OzoneDissolverPower" => vo.Sterilizing.OzoneDissolverPower.ToString(),
"Sterilizing.ExcessOzoneDestroyerPower" => vo.Sterilizing.ExcessOzoneDestroyerPower.ToString(),
_ => null
};
}
private string ResolveTankOrTime(WaterQualityVO vo, string fieldName, TimeSpan? bucket = null) private string ResolveTankOrTime(WaterQualityVO vo, string fieldName, TimeSpan? bucket = null)
{ {
return fieldName switch return fieldName switch
@ -370,23 +478,47 @@ namespace SmartAquaViewer.ViewModel
}; };
} }
private static string FormatBucket(DateTime t, TimeSpan bucket) private double? ResolveUvPowerPerId(WaterQualityVO r, string fieldName)
{ {
if (bucket >= TimeSpan.FromDays(7)) return t.ToString("yyyy-MM-dd"); // 케이스 A
if (bucket >= TimeSpan.FromDays(1)) return t.ToString("MM-dd"); if (fieldName.StartsWith("Sterilizing.UVSterilizerPower[id="))
return t.ToString("HH:00"); {
var id = fieldName
.Replace("Sterilizing.UVSterilizerPower[id=", "")
.TrimEnd(']');
if (r?.Sterilizing?.UVSterilizerId == id)
return MapStatus(r.Sterilizing.UVSterilizerPower);
// 해당 시점에 이 ID의 레코드가 없으면 null (직전값 유지 로직에서 커버)
return null;
}
return null;
} }
private string ResolveGroupKey(WaterQualityVO vo, FieldItem gf) private static double MapStatus(object? v)
{ {
return gf.Name switch return v switch
{ {
"수조" or "Tank" => vo.Tank.Number.ToString(), bool b => b ? 1 : 0,
"시간" or "RecordedTime" => vo.RecordedTime.ToString("HH:mm"), int i => i, // 이미 0/1/2로 들어온 경우
_ => gf.Name string s => s.Equals("True", StringComparison.OrdinalIgnoreCase) ? 1 :
s.Equals("False", StringComparison.OrdinalIgnoreCase) ? 0 :
s.Equals("Normal", StringComparison.OrdinalIgnoreCase) ? 1 :
s.Equals("Error", StringComparison.OrdinalIgnoreCase) ? 2 : -1,
_ => -1
}; };
} }
private static string FormatBucket(DateTime t, TimeSpan bucket)
{
if (bucket >= TimeSpan.FromDays(7)) return t.ToString("yyyy-MM-dd");
if (bucket >= TimeSpan.FromDays(1)) return t.ToString("MM-dd");
return t.ToString("HH:00");
}
private double Percentile(IList<double> values, double p) private double Percentile(IList<double> values, double p)
{ {
if (values == null || values.Count == 0) return double.NaN; if (values == null || values.Count == 0) return double.NaN;
@ -400,24 +532,6 @@ namespace SmartAquaViewer.ViewModel
return (double)(values[i] + (values[i + 1] - values[i]) * frac); return (double)(values[i] + (values[i + 1] - values[i]) * frac);
} }
private (double lw, double q1, double med, double q3, double uw, List<double> outs) Summarize(IList<double> values)
{
double q1 = Percentile(values, 0.25);
double med = Percentile(values, 0.50);
double q3 = Percentile(values, 0.75);
double iqr = q3 - q1;
double lf = q1 - 1.5 * iqr;
double uf = q3 + 1.5 * iqr;
var inliers = values.Where(v => v >= lf && v <= uf).ToList();
var outs = values.Where(v => v < lf || v > uf).ToList();
double lw = (inliers.Count > 0 ? inliers.First() : values.First());
double uw = (inliers.Count > 0 ? inliers.Last() : values.Last());
return (lw, q1, med, q3, uw, outs);
}
// 단순 선형회귀 계산 // 단순 선형회귀 계산
private (double Slope, double Intercept)? LinearRegression(List<DataPoint> points) private (double Slope, double Intercept)? LinearRegression(List<DataPoint> points)
{ {
@ -438,6 +552,18 @@ namespace SmartAquaViewer.ViewModel
return (slope, intercept); return (slope, intercept);
} }
private double MapDeviceStatus(string status)
{
return status switch
{
"True" => 1, // 전원 ON
"False" => 0, // 전원 OFF
"Normal" => 1, // 정상
"Error" => 2, // 오류
_ => -1 // 알 수 없음
};
}
private class KeyComparer : IComparer<string> private class KeyComparer : IComparer<string>
{ {
public int Compare(string? a, string? b) public int Compare(string? a, string? b)

@ -13,7 +13,6 @@ using System.Windows.Threading;
using SmartAquaViewer.Controls; using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis; using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model; using SmartAquaViewer.Model;
using static SmartAquaViewer.Model.Enums;
namespace SmartAquaViewer.ViewModel namespace SmartAquaViewer.ViewModel
{ {
@ -22,6 +21,7 @@ namespace SmartAquaViewer.ViewModel
public string? Name { get; init; } // 바인딩 경로 키 (예: "Tank.DOValue") public string? Name { get; init; } // 바인딩 경로 키 (예: "Tank.DOValue")
public string? Display { get; init; } // UI 표시명 (예: "DO (mg/L)") public string? Display { get; init; } // UI 표시명 (예: "DO (mg/L)")
public Type? DataType { get; init; } // typeof(double), typeof(DateTime) 등 public Type? DataType { get; init; } // typeof(double), typeof(DateTime) 등
public StepFieldKind Kind { get; init; }
} }
public class MonitoringViewModel : INotifyPropertyChanged public class MonitoringViewModel : INotifyPropertyChanged
@ -100,6 +100,22 @@ namespace SmartAquaViewer.ViewModel
} }
} }
private StepFieldKind _selectedKind = StepFieldKind.Sensor; // 기본값은 센서
public StepFieldKind SelectedKind
{
get => _selectedKind;
set
{
if (_selectedKind != value)
{
_selectedKind = value;
OnPropertyChanged();
// 라디오 변경 시 Y 후보 재구성
RebuildFieldCandidates();
}
}
}
public bool IsTankAndLine public bool IsTankAndLine
{ {
get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE); get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE);
@ -209,6 +225,7 @@ namespace SmartAquaViewer.ViewModel
} }
} }
} }
#endregion #endregion
public ICommand ChangeDrawerStatusCommand { get; } public ICommand ChangeDrawerStatusCommand { get; }
@ -272,6 +289,16 @@ namespace SmartAquaViewer.ViewModel
var showRegression = ShowRegression; var showRegression = ShowRegression;
GraphControlVM.SetScatterPlot(WaterQualityList, xFieldScatter, yFiledScatter, markerSIze, showRegression); GraphControlVM.SetScatterPlot(WaterQualityList, xFieldScatter, yFiledScatter, markerSIze, showRegression);
break; break;
case GraphType.STEP:
var xFieldStep = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null;
var tFieldsStep = SelectedYFields;
var yFiledStep = SelectedYField;
var showMarkerStep = ShowMarkers;
if (SelectedKind.Equals(StepFieldKind.Status))
GraphControlVM.SetStatusSeriesStopPlot(WaterQualityList, yFiledStep, showMarkerStep);
else
GraphControlVM.SetStepPlot(WaterQualityList, SelectedTab, xFieldStep, tFieldsStep, showMarkerStep);
break;
default: default:
break; break;
} }
@ -322,30 +349,63 @@ namespace SmartAquaViewer.ViewModel
if (SelectedTab == MonitorTab.Tank) if (SelectedTab == MonitorTab.Tank)
{ {
AvailableFields.Add(new FieldItem { Name = "Tank.Number", Display = "수조", DataType = typeof(int) }); AvailableFields.Add(new FieldItem { Name = "Tank.Number", Display = "수조", DataType = typeof(int), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.DOValue", Display = "DO (mg/L)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Tank.DOValue", Display = "DO (mg/L)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.PH", Display = "pH", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Tank.PH", Display = "pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.ORP", Display = "ORP (mV)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Tank.ORP", Display = "ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.Temperature", Display = "온도 (℃)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Tank.Temperature", Display = "온도 (℃)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.FlowRate", Display = "유량 (m³/s)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Tank.FlowRate", Display = "유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
} }
else if (SelectedTab == MonitorTab.Filter) else if (SelectedTab == MonitorTab.Filter)
{ {
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpPH", Display = "섬프 pH", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Filtering.SumpPH", Display = "섬프 pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpORP", Display = "섬프 ORP (mV)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Filtering.SumpORP", Display = "섬프 ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpWaterLevel", Display = "섬프 수위 (m)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Filtering.SumpWaterLevel", Display = "섬프 수위 (m)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpFlowRate", Display = "섬프 유량 (m³/s)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Filtering.SumpFlowRate", Display = "섬프 유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpTemperature", Display = "섬프 수온 (°C)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Filtering.SumpTemperature", Display = "섬프 수온 (°C)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.FlowRate", Display = "순환펌프 유량", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Filtering.FlowRate", Display = "순환펌프 유량", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpTemperature", Display = "히트펌프 온도", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpTemperature", Display = "히트펌프 온도", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SandFilterPower", Display = "모래여과기 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.CirculationPumpPower", Display = "순환펌프 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.InverterControllerStatus", Display = "인버터 제어기 상태", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpPower", Display = "히트펌프 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.AirBlowerPower", Display = "에어브로와 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
} }
else // Sterilizer else // Sterilizer
{ {
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPressure", Display = "용해장치 압력 (kPa)", DataType = typeof(double) }); AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPressure", Display = "용해장치 압력 (kPa)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
// 필요한 다른 수치 필드들 추가 AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneGeneratorPower", Display = "오존 발생기 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPower", Display = "오존용해장치 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.ExcessOzoneDestroyerPower", Display = "배오존장치 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AddUvPowerFieldsPerId(WaterQualityList);
} }
} }
// rows: 현재 그리려는 데이터(필터링/정렬 반영된)
private void AddUvPowerFieldsPerId(IEnumerable<WaterQualityVO> rows)
{
// 케이스 A: 한 행이 UV 한 대의 상태를 담는 스키마
var idsA = rows
.Select(r => r?.Sterilizing?.UVSterilizerId)
.Where(id => !string.IsNullOrWhiteSpace(id))
.Distinct()
.ToList();
foreach (var id in idsA)
{
// Name 규칙: "Sterilizing.UVSterilizerPower[id=XXX]"
AvailableFields.Add(new FieldItem
{
Name = $"Sterilizing.UVSterilizerPower[id={id}]",
Display = $"자외선 살균기 {id} 전원",
DataType = typeof(int),
Kind = StepFieldKind.Status
});
}
}
// 그래프 타입이 바뀔 때 후보/기본 선택 재구성 // 그래프 타입이 바뀔 때 후보/기본 선택 재구성
private void RebuildFieldCandidates() private void RebuildFieldCandidates()
{ {
@ -364,9 +424,22 @@ namespace SmartAquaViewer.ViewModel
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime)) SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault(); ?? AvailableFields.FirstOrDefault();
IEnumerable<FieldItem> src = AvailableFields.Where(f => f.Kind == SelectedKind);
if (SelectedGraphType is GraphType.LINE or GraphType.SCATTER or GraphType.BOX)
{
// 수치형만 (LINE/SCATTER/BOX는 연속값 위주)
src = src.Where(f => f.DataType == typeof(double));
}
else if (SelectedGraphType == GraphType.STEP)
{
// STEP은 상태 전환에 잘 맞음: int/bool 위주
src = src.Where(f => f.DataType == typeof(int) || f.DataType == typeof(bool) || f.DataType == typeof(double));
// (상태가 double로 들어오는 경우도 있을 수 있어 double 허용)
}
// Y축 후보: 수치형 // Y축 후보: 수치형
foreach (var f in AvailableFields.Where(f => f.DataType == typeof(double))) foreach (var f in src) YFieldCandidates.Add(f);
YFieldCandidates.Add(f);
// 기본 선택 세팅 (타입별) // 기본 선택 세팅 (타입별)
SelectedYFields.Clear(); SelectedYFields.Clear();

Loading…
Cancel
Save