diff --git a/SmartAquaViewer/Classes/Converter.cs b/SmartAquaViewer/Classes/Converter.cs
new file mode 100644
index 0000000..19c258c
--- /dev/null
+++ b/SmartAquaViewer/Classes/Converter.cs
@@ -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;
+ }
+ }
+}
diff --git a/SmartAquaViewer/Controls/SegmentedControl.xaml.cs b/SmartAquaViewer/Controls/SegmentedControl.xaml.cs
index 2015411..187399a 100644
--- a/SmartAquaViewer/Controls/SegmentedControl.xaml.cs
+++ b/SmartAquaViewer/Controls/SegmentedControl.xaml.cs
@@ -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
{
diff --git a/SmartAquaViewer/Model/Enums.cs b/SmartAquaViewer/Model/Enums.cs
index 413aaf7..7f4c8f5 100644
--- a/SmartAquaViewer/Model/Enums.cs
+++ b/SmartAquaViewer/Model/Enums.cs
@@ -6,21 +6,30 @@ using System.Threading.Tasks;
namespace SmartAquaViewer.Model
{
- public class Enums
+ public enum MonitorTab
{
- public enum MonitorTab
- {
- Tank,
- Filter,
- Sterilizer
- }
+ Tank,
+ Filter,
+ Sterilizer
+ }
+
+ public enum GraphType
+ {
+ LINE,
+ BOX,
+ SCATTER,
+ STEP
+ }
- public enum GraphType
- {
- LINE,
- BOX,
- SCATTER,
- STEP
- }
+ public enum StepFieldKind
+ {
+ Status, // 전원/상태
+ Sensor // 센서 값
+ }
+
+ public enum PowerStatus
+ {
+ Off,
+ On
}
}
diff --git a/SmartAquaViewer/View/MonitoringView.xaml b/SmartAquaViewer/View/MonitoringView.xaml
index 9b055a4..3d3f3f6 100644
--- a/SmartAquaViewer/View/MonitoringView.xaml
+++ b/SmartAquaViewer/View/MonitoringView.xaml
@@ -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">
+
+
+
+
+
+
@@ -83,7 +91,7 @@
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
-
@@ -95,17 +103,17 @@
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
-
-
-
@@ -122,17 +130,17 @@
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
-
-
-
-
@@ -254,8 +262,8 @@
-
-
+
+
@@ -287,7 +295,81 @@
Height="40" Style="{StaticResource ComboBoxStyle}"/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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}"/>
@@ -307,10 +389,6 @@
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
-
diff --git a/SmartAquaViewer/View/MonitoringView.xaml.cs b/SmartAquaViewer/View/MonitoringView.xaml.cs
index e23503d..5e8d16d 100644
--- a/SmartAquaViewer/View/MonitoringView.xaml.cs
+++ b/SmartAquaViewer/View/MonitoringView.xaml.cs
@@ -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
{
diff --git a/SmartAquaViewer/ViewModel/GraphControlViewModel.cs b/SmartAquaViewer/ViewModel/GraphControlViewModel.cs
index 87f327c..58b917a 100644
--- a/SmartAquaViewer/ViewModel/GraphControlViewModel.cs
+++ b/SmartAquaViewer/ViewModel/GraphControlViewModel.cs
@@ -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 rows,
+ MonitorTab selectedTab,
+ FieldItem xAxisField,
+ ObservableCollection 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 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 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 outs) Summarize(IList 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 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
{
public int Compare(string? a, string? b)
diff --git a/SmartAquaViewer/ViewModel/MonitoringViewModel.cs b/SmartAquaViewer/ViewModel/MonitoringViewModel.cs
index deefae8..4da8a55 100644
--- a/SmartAquaViewer/ViewModel/MonitoringViewModel.cs
+++ b/SmartAquaViewer/ViewModel/MonitoringViewModel.cs
@@ -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 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 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();