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.Navigation;
using System.Windows.Shapes;
using static SmartAquaViewer.Model.Enums;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.Controls
{

@ -6,8 +6,6 @@ using System.Threading.Tasks;
namespace SmartAquaViewer.Model
{
public class Enums
{
public enum MonitorTab
{
Tank,
@ -22,5 +20,16 @@ namespace SmartAquaViewer.Model
SCATTER,
STEP
}
public enum StepFieldKind
{
Status, // 전원/상태
Sensor // 센서 값
}
public enum PowerStatus
{
Off,
On
}
}

@ -6,9 +6,17 @@
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:control="clr-namespace:SmartAquaViewer.Controls"
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"
mc:Ignorable="d"
d:DesignHeight="940" d:DesignWidth="1650">
<UserControl.Resources>
<classes:EnumEqualsConverter x:Key="EnumEqualsConverter"/>
<classes:BoolToPowerConverter x:Key="BoolToPowerConverter"/>
</UserControl.Resources>
<Border>
<md:DrawerHost BottomDrawerBackground="#122136" IsBottomDrawerOpen="{Binding IsOpenMode}" OpenMode="Standard">
<Grid Background="#243851">
@ -83,7 +91,7 @@
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="모래여과기 전원" Binding="{Binding Filtering.SandFilterPower}"
<DataGridTextColumn Header="모래여과기 전원" Binding="{Binding Filtering.SandFilterPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 pH" Binding="{Binding Filtering.SumpPH}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
@ -95,17 +103,17 @@
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 수온(°C)" Binding="{Binding Filtering.SumpTemperature, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="순환펌프 전원" Binding="{Binding Filtering.CirculationPumpPower}"
<DataGridTextColumn Header="순환펌프 전원" Binding="{Binding Filtering.CirculationPumpPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="인버터 제어기 상태" Binding="{Binding Filtering.InverterControllerStatus}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="순환펌프 유량(m³/s)" Binding="{Binding Filtering.FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="히트펌프 전원" Binding="{Binding Filtering.HeatPumpPower}"
<DataGridTextColumn Header="히트펌프 전원" Binding="{Binding Filtering.HeatPumpPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="히트펌프 온도(°C)" Binding="{Binding Filtering.HeatPumpTemperature, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="에어브로와 전원" Binding="{Binding Filtering.AirBlowerPower}"
<DataGridTextColumn Header="에어브로와 전원" Binding="{Binding Filtering.AirBlowerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns>
</DataGrid>
@ -122,17 +130,17 @@
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존 발생기 전원" Binding="{Binding Sterilizing.OzoneGeneratorPower}"
<DataGridTextColumn Header="오존 발생기 전원" Binding="{Binding Sterilizing.OzoneGeneratorPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 ID" Binding="{Binding Sterilizing.UVSterilizerId}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 전원" Binding="{Binding Sterilizing.UVSterilizerPower}"
<DataGridTextColumn Header="자외선 살균기 전원" Binding="{Binding Sterilizing.UVSterilizerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 전원" Binding="{Binding Sterilizing.OzoneDissolverPower}"
<DataGridTextColumn Header="오존용해장치 전원" Binding="{Binding Sterilizing.OzoneDissolverPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 압력(kPa)" Binding="{Binding Sterilizing.OzoneDissolverPressure, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="배오존장치 전원" Binding="{Binding Sterilizing.ExcessOzoneDestroyerPower}"
<DataGridTextColumn Header="배오존장치 전원" Binding="{Binding Sterilizing.ExcessOzoneDestroyerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns>
</DataGrid>
@ -254,8 +262,8 @@
</Style>
</Grid.Resources>
<!-- LINE/STEP: 다중 선택 -->
<StackPanel Style="{StaticResource VisibleWhenTrue}">
<!-- LINE -->
<StackPanel Style="{StaticResource VisibleWhenLine}">
<StackPanel Style="{StaticResource VisibleWhenTankNLine}">
<TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
@ -287,7 +295,81 @@
Height="40" Style="{StaticResource ComboBoxStyle}"/>
</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 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"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<!-- SelectedItems 바인딩을 위한 간단 Behavior는 아래 3) 참고 -->
@ -297,7 +379,7 @@
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
FontSize="14" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</StackPanel>
@ -307,10 +389,6 @@
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>

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

@ -11,7 +11,7 @@ using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using SmartAquaViewer.DataAnalysis;
using static SmartAquaViewer.Model.Enums;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
@ -315,6 +315,98 @@ namespace SmartAquaViewer.ViewModel
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)
{
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)
{
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");
if (bucket >= TimeSpan.FromDays(1)) return t.ToString("MM-dd");
return t.ToString("HH:00");
// 케이스 A
if (fieldName.StartsWith("Sterilizing.UVSterilizerPower[id="))
{
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(),
"시간" or "RecordedTime" => vo.RecordedTime.ToString("HH:mm"),
_ => gf.Name
bool b => b ? 1 : 0,
int i => i, // 이미 0/1/2로 들어온 경우
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)
{
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);
}
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)
{
@ -438,6 +552,18 @@ namespace SmartAquaViewer.ViewModel
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>
{
public int Compare(string? a, string? b)

@ -13,7 +13,6 @@ using System.Windows.Threading;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
using static SmartAquaViewer.Model.Enums;
namespace SmartAquaViewer.ViewModel
{
@ -22,6 +21,7 @@ namespace SmartAquaViewer.ViewModel
public string? Name { get; init; } // 바인딩 경로 키 (예: "Tank.DOValue")
public string? Display { get; init; } // UI 표시명 (예: "DO (mg/L)")
public Type? DataType { get; init; } // typeof(double), typeof(DateTime) 등
public StepFieldKind Kind { get; init; }
}
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
{
get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE);
@ -209,6 +225,7 @@ namespace SmartAquaViewer.ViewModel
}
}
}
#endregion
public ICommand ChangeDrawerStatusCommand { get; }
@ -272,6 +289,16 @@ namespace SmartAquaViewer.ViewModel
var showRegression = ShowRegression;
GraphControlVM.SetScatterPlot(WaterQualityList, xFieldScatter, yFiledScatter, markerSIze, showRegression);
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:
break;
}
@ -322,30 +349,63 @@ namespace SmartAquaViewer.ViewModel
if (SelectedTab == MonitorTab.Tank)
{
AvailableFields.Add(new FieldItem { Name = "Tank.Number", Display = "수조", DataType = typeof(int) });
AvailableFields.Add(new FieldItem { Name = "Tank.DOValue", Display = "DO (mg/L)", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Tank.PH", Display = "pH", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Tank.ORP", Display = "ORP (mV)", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Tank.Temperature", Display = "온도 (℃)", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Tank.FlowRate", Display = "유량 (m³/s)", DataType = typeof(double) });
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), Kind = StepFieldKind.Sensor });
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), Kind = StepFieldKind.Sensor });
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), Kind = StepFieldKind.Sensor });
}
else if (SelectedTab == MonitorTab.Filter)
{
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpPH", Display = "섬프 pH", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpORP", Display = "섬프 ORP (mV)", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpWaterLevel", Display = "섬프 수위 (m)", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpFlowRate", Display = "섬프 유량 (m³/s)", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpTemperature", Display = "섬프 수온 (°C)", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Filtering.FlowRate", Display = "순환펌프 유량", DataType = typeof(double) });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpTemperature", Display = "히트펌프 온도", 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), Kind = StepFieldKind.Sensor });
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), Kind = StepFieldKind.Sensor });
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), Kind = StepFieldKind.Sensor });
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
{
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()
{
@ -364,9 +424,22 @@ namespace SmartAquaViewer.ViewModel
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? 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축 후보: 수치형
foreach (var f in AvailableFields.Where(f => f.DataType == typeof(double)))
YFieldCandidates.Add(f);
foreach (var f in src) YFieldCandidates.Add(f);
// 기본 선택 세팅 (타입별)
SelectedYFields.Clear();

Loading…
Cancel
Save