From e79451459235c431a49eeb55b080563e210a906b Mon Sep 17 00:00:00 2001 From: hj615 Date: Wed, 20 Aug 2025 15:54:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Step=20=EA=B7=B8=EB=9E=98=ED=94=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SmartAquaViewer/Classes/Converter.cs | 34 ++++ .../Controls/SegmentedControl.xaml.cs | 2 +- SmartAquaViewer/Model/Enums.cs | 37 ++-- SmartAquaViewer/View/MonitoringView.xaml | 110 +++++++++-- SmartAquaViewer/View/MonitoringView.xaml.cs | 2 +- .../ViewModel/GraphControlViewModel.cs | 182 +++++++++++++++--- .../ViewModel/MonitoringViewModel.cs | 109 +++++++++-- 7 files changed, 398 insertions(+), 78 deletions(-) create mode 100644 SmartAquaViewer/Classes/Converter.cs 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();