fix: 상자그림 생성 기능 수정

prototype
HyungJune Kim 10 months ago
parent 8858bafd8b
commit 9a3ae0cda2

@ -22,6 +22,8 @@ namespace SmartAquaViewer.DataAnalysis
/// </summary> /// </summary>
public WaterTank Tank { get; set; } = new(); public WaterTank Tank { get; set; } = new();
public List<WaterTank> Tanks { get; set; } = new();
/// <summary> /// <summary>
/// 여과 시스템 /// 여과 시스템
/// </summary> /// </summary>
@ -111,7 +113,6 @@ namespace SmartAquaViewer.DataAnalysis
{ {
DateTime ts = start.AddSeconds(stepSeconds * i); DateTime ts = start.AddSeconds(stepSeconds * i);
var vo = new WaterQualityVO var vo = new WaterQualityVO
{ {
RecordedTime = ts, RecordedTime = ts,
@ -123,6 +124,30 @@ namespace SmartAquaViewer.DataAnalysis
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2), temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2) flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)
), ),
Tanks = new List<WaterTank>()
{
new WaterTank(
number: 1,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
new WaterTank(
number: 2,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
new WaterTank(
number: 3,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
},
Filtering = new FilteringSystem( Filtering = new FilteringSystem(
sandFilterPower: rand.Next(0, 2) == 1, sandFilterPower: rand.Next(0, 2) == 1,
sandFilterEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2), sandFilterEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),

@ -32,28 +32,14 @@
SelectedTab="{Binding SelectedTab, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> SelectedTab="{Binding SelectedTab, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid> </Grid>
<ScrollViewer x:Name="svTanks" Grid.Row="1" Margin="20 20 20 40" <DataGrid x:Name="dgTanks" ItemsSource="{Binding TanksByTimes}"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalAlignment="Center">
<ItemsControl ItemsSource="{Binding TankGroups}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="3"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="20 0" HorizontalAlignment="Center" >
<TextBlock Text="{Binding Key, StringFormat=수조 {0}}"
FontSize="20" FontWeight="Bold" Foreground="White"
Margin="0 0 0 10"/>
<!-- Value(= ObservableCollection<WaterQualityVO>)로 DataGrid -->
<DataGrid ItemsSource="{Binding Value}"
Style="{StaticResource DataGridStyle}" Style="{StaticResource DataGridStyle}"
RowStyle="{StaticResource DataGridRowStyle}" RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"> ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"
Grid.Row="1" Margin="20 20 20 40"
ColumnWidth="*"
Background="Transparent"
HorizontalAlignment="Stretch">
<DataGrid.Columns> <DataGrid.Columns>
<!-- 측정 시각 --> <!-- 측정 시각 -->
<DataGridTextColumn <DataGridTextColumn
@ -62,29 +48,57 @@
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<!-- Tank 값들 --> <!-- Tank 값들 -->
<DataGridTextColumn Header="DO(mg/L)" Binding="{Binding Tank.DOValue, StringFormat=\{0:F2\}}" <DataGridTextColumn Header="번호" Binding="{Binding Tanks[0].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="DO(mg/L)" Binding="{Binding Tanks[0].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="pH" Binding="{Binding Tanks[0].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="ORP(mV)" Binding="{Binding Tanks[0].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="온도(℃)" Binding="{Binding Tanks[0].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="유량(m³/s)" Binding="{Binding Tanks[0].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Width="5"/>
<DataGridTextColumn Header="번호" Binding="{Binding Tanks[1].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="DO(mg/L)" Binding="{Binding Tanks[1].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="pH" Binding="{Binding Tanks[1].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="ORP(mV)" Binding="{Binding Tanks[1].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="pH" Binding="{Binding Tank.PH, StringFormat=\{0:F2\}}" <DataGridTextColumn Header="온도(℃)" Binding="{Binding Tanks[1].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="유량(m³/s)" Binding="{Binding Tanks[1].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Width="5"/>
<DataGridTextColumn Header="번호" Binding="{Binding Tanks[2].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="ORP(mV)" Binding="{Binding Tank.ORP, StringFormat=\{0:F0\}}" <DataGridTextColumn Header="DO(mg/L)" Binding="{Binding Tanks[2].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="온도(℃)" Binding="{Binding Tank.Temperature, StringFormat=\{0:F1\}}" <DataGridTextColumn Header="pH" Binding="{Binding Tanks[2].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="유량(m³/s)" Binding="{Binding Tank.FlowRate, StringFormat=\{0:F2\}}" <DataGridTextColumn Header="ORP(mV)" Binding="{Binding Tanks[2].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="온도(℃)" Binding="{Binding Tanks[2].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="유량(m³/s)" Binding="{Binding Tanks[2].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</StackPanel>
</DataTemplate> <DataGrid ItemsSource="{Binding WaterQualityList}" x:Name="dgFilter"
</ItemsControl.ItemTemplate> Style="{StaticResource DataGridStyle}" ColumnWidth="*"
</ItemsControl> Grid.Row="1" Margin="20 20 20 40"
</ScrollViewer> Background="Transparent"
RowStyle="{StaticResource DataGridRowStyle}"
<ScrollViewer x:Name="svFilter" Grid.Row="1" Margin="20 20 20 40" ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalAlignment="Center" Visibility="Collapsed">
<DataGrid ItemsSource="{Binding WaterQualityList}" Style="{StaticResource DataGridStyle}"
RowStyle="{StaticResource DataGridRowStyle}" ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn <DataGridTextColumn
Header="시간" Header="시간"
@ -117,13 +131,14 @@
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</ScrollViewer>
<ScrollViewer x:Name="svSterilizer" Grid.Row="1" Margin="20 20 20 40" <DataGrid ItemsSource="{Binding WaterQualityList}" x:Name="dgSterilizer"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Style="{StaticResource DataGridStyle}" ColumnWidth="*"
HorizontalAlignment="Center" Visibility="Collapsed"> Grid.Row="1" Margin="20 20 20 40"
<DataGrid ItemsSource="{Binding WaterQualityList}" Style="{StaticResource DataGridStyle}" Background="Transparent"
RowStyle="{StaticResource DataGridRowStyle}" ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"> VerticalScrollBarVisibility="Auto"
RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn <DataGridTextColumn
Header="시간" Header="시간"
@ -144,7 +159,6 @@
ElementStyle="{StaticResource DataGridElmenetStyle}"/> ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</ScrollViewer>
<Grid Grid.Row="1" VerticalAlignment="Bottom"> <Grid Grid.Row="1" VerticalAlignment="Bottom">
<Button Name="btnVisibilityDown" Tag="down" <Button Name="btnVisibilityDown" Tag="down"
@ -202,7 +216,24 @@
DisplayMemberPath="Display"/> DisplayMemberPath="Display"/>
</Grid> </Grid>
<Grid Margin="15 10 15 0"> <StackPanel Margin="15 15 15 0">
<TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Border CornerRadius="10" Background="White" Margin="0 5 0 10">
<ListBox ItemsSource="{Binding TankGroups}"
DisplayMemberPath="Key"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedDictionary="{Binding SelectedWaterTanks, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</StackPanel>
<Grid Margin="15 0">
<Grid.Resources> <Grid.Resources>
<Style TargetType="FrameworkElement"> <Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/> <Setter Property="Visibility" Value="Collapsed"/>
@ -264,23 +295,6 @@
<!-- LINE --> <!-- LINE -->
<StackPanel Style="{StaticResource VisibleWhenLine}"> <StackPanel Style="{StaticResource VisibleWhenLine}">
<StackPanel Style="{StaticResource VisibleWhenTankNLine}">
<TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Border CornerRadius="10" Background="White" Margin="0 5 0 10">
<ListBox ItemsSource="{Binding TankGroups}"
DisplayMemberPath="Key"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedWaterTanks, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</StackPanel>
<Grid Style="{StaticResource VisibleWhenLine}"> <Grid Style="{StaticResource VisibleWhenLine}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/> <ColumnDefinition Width="80"/>
@ -301,6 +315,11 @@
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 ShowLegends}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<!--<CheckBox Content="스무딩" IsChecked="{Binding UseSmoothing}" <!--<CheckBox Content="스무딩" IsChecked="{Binding UseSmoothing}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White" FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
@ -389,6 +408,10 @@
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 ShowLegends}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

@ -28,17 +28,17 @@ namespace SmartAquaViewer.View
{ {
private MonitoringViewModel? monitoringViewModel; private MonitoringViewModel? monitoringViewModel;
private readonly Dictionary<MonitorTab, ScrollViewer> _tabMap; private readonly Dictionary<MonitorTab, UIElement> _tabMap;
public MonitoringView() public MonitoringView()
{ {
InitializeComponent(); InitializeComponent();
_tabMap = new Dictionary<MonitorTab, ScrollViewer> _tabMap = new Dictionary<MonitorTab, UIElement>
{ {
{ MonitorTab.Tank, svTanks }, { MonitorTab.Tank, dgTanks },
{ MonitorTab.Filter, svFilter }, { MonitorTab.Filter, dgFilter },
{ MonitorTab.Sterilizer, svSterilizer } { MonitorTab.Sterilizer, dgSterilizer }
}; };
Loaded += MonitoringView_Loaded; Loaded += MonitoringView_Loaded;
@ -60,9 +60,9 @@ namespace SmartAquaViewer.View
private void SetActiveTab(MonitorTab tab) private void SetActiveTab(MonitorTab tab)
{ {
// 전부 Collapsed // 전부 Collapsed
svTanks.Visibility = Visibility.Collapsed; dgTanks.Visibility = Visibility.Collapsed;
svFilter.Visibility = Visibility.Collapsed; dgFilter.Visibility = Visibility.Collapsed;
svSterilizer.Visibility = Visibility.Collapsed; dgSterilizer.Visibility = Visibility.Collapsed;
// 대상만 Visible // 대상만 Visible
if (_tabMap.TryGetValue(tab, out var target)) if (_tabMap.TryGetValue(tab, out var target))

@ -1,13 +1,7 @@
using System; using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using OxyPlot.Axes;
using SmartAquaViewer.Controls; using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis; using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model; using SmartAquaViewer.Model;

@ -1,11 +1,4 @@
using System; using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.VisualBasic;
using OxyPlot; using OxyPlot;
using OxyPlot.Axes; using OxyPlot.Axes;
using OxyPlot.Legends; using OxyPlot.Legends;
@ -30,9 +23,10 @@ namespace SmartAquaViewer.ViewModel
Model.TextColor = OxyColors.Black; Model.TextColor = OxyColors.Black;
} }
public void SetTankLineGraph(Dictionary<int, ObservableCollection<WaterQualityVO>> collection, public void SetTankLineGraph(
Dictionary<int, ObservableCollection<WaterQualityVO>> collection,
FieldItem? xField, FieldItem? yField, FieldItem? xField, FieldItem? yField,
bool isMarker) bool isMarker, bool showLegends)
{ {
Model.Series.Clear(); Model.Series.Clear();
Model.Axes.Clear(); Model.Axes.Clear();
@ -57,9 +51,10 @@ namespace SmartAquaViewer.ViewModel
Model.Axes.Add(xAxis); Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis); Model.Axes.Add(yAxis);
foreach (var (tankNum, data) in collection.OrderBy(x => x.Key)) foreach (var (tankNum, datas) in collection.OrderBy(x => x.Key))
{ {
if (data == null || data.Count == 0) continue; if (datas == null || datas.Count == 0)
continue;
var series = new LineSeries() var series = new LineSeries()
{ {
@ -68,13 +63,14 @@ namespace SmartAquaViewer.ViewModel
MarkerSize = isMarker ? 3 : 0 MarkerSize = isMarker ? 3 : 0
}; };
foreach (var r in data.OrderBy(r => r.RecordedTime)) foreach (var data in datas.OrderBy(d => d.RecordedTime))
{ {
var y = ResolveTank(r, yField.Name!); var tank = data.Tanks.Find(t => t.Number.Equals(tankNum));
var y = ResolveTank(tank, yField.Name!);
if (!y.HasValue) continue; if (!y.HasValue) continue;
series.Points.Add(new DataPoint( series.Points.Add(new DataPoint(
DateTimeAxis.ToDouble(r.RecordedTime), DateTimeAxis.ToDouble(data.RecordedTime), // 여기서 recordedTime 사용
y.Value)); y.Value));
} }
@ -87,6 +83,16 @@ namespace SmartAquaViewer.ViewModel
} }
} }
Model.Legends.Clear();
Model.IsLegendVisible = showLegends;
Model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.RightTop,
LegendOrientation = LegendOrientation.Vertical,
LegendTitle = "수조",
TextColor = OxyColors.Black
});
Model.InvalidatePlot(true); Model.InvalidatePlot(true);
} }
@ -219,7 +225,8 @@ namespace SmartAquaViewer.ViewModel
} }
public void SetBoxPlot( public void SetBoxPlot(
List<WaterQualityVO> rows, ReadOnlyObservableCollection<WaterQualityVO> collection,
List<int>? selectedTankNums,
FieldItem xAxisKind, // 시간 or 수조 FieldItem xAxisKind, // 시간 or 수조
FieldItem valueField, // 값 필드 FieldItem valueField, // 값 필드
double boxWidth, // 박스 너비 double boxWidth, // 박스 너비
@ -248,62 +255,77 @@ namespace SmartAquaViewer.ViewModel
Model.Axes.Add(xAxis); Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis); Model.Axes.Add(yAxis);
var series = new BoxPlotSeries() var timeBuckets = collection
.Select(w => FloorToBucket(w.RecordedTime, (TimeSpan)timeBucket))
.Distinct()
.OrderBy(t => t)
.ToList();
// X축 라벨
xAxis.Labels.AddRange(timeBuckets.Select(t => t.ToString("MM-dd HH:mm")));
var allTankIds = collection.SelectMany(w => w.Tanks).Select(t => t.Number).Distinct().OrderBy(id => id).ToList();
var tankIds = (selectedTankNums == null || !selectedTankNums.Any())
? allTankIds
: allTankIds.Where(id => selectedTankNums.Contains(id)).ToList();
if (tankIds.Count == 0 || timeBuckets.Count == 0)
{ {
BoxWidth = boxWidth Model.InvalidatePlot(true);
}; return;
}
if (rows == null || rows.Count == 0) return; // 3) 팔레트
var colors = OxyPalettes.HueDistinct(tankIds.Count).Colors;
var bucket = timeBucket ?? TimeSpan.FromHours(1); for (int k = 0; k < tankIds.Count; k++)
{
int tankId = tankIds[k];
var col = rows var series = new BoxPlotSeries
.OrderBy(r => r.RecordedTime) {
.GroupBy(g => ResolveTankOrTime(g, xAxisKind.Name, bucket)) Title = $"Tank {tankId}",
.ToDictionary(g => g.Key, g => g.ToList()); BoxWidth = boxWidth,
Fill = OxyColor.FromAColor(160, colors[k]),
Stroke = colors[k],
StrokeThickness = 1
};
foreach(var (key, data) in col.OrderBy(x => x.Key)) for (int ti = 0; ti < timeBuckets.Count; ti++)
{ {
// 1) 수조/시간별 값 리스트 var bucketTime = timeBuckets[ti];
var values = data
.Select(d => ResolveTank(d, valueField.Name!)) // 해당 시간 버킷 + 해당 수조 + 선택 필드 값 모으기
.OfType<double>() // object→double var values = collection
.Where(v => !double.IsNaN(v) && !double.IsInfinity(v)) .Where(w => FloorToBucket(w.RecordedTime, (TimeSpan)timeBucket!) == bucketTime)
.OrderBy(v => v) .SelectMany(w => w.Tanks)
.Where(t => t.Number == tankId)
.Select(t => ResolveTank(t, valueField.Name!)) // 선택 필드 동적 접근
.Where(v => v.HasValue)
.Select(v => v.Value)
.ToList(); .ToList();
if (values.Count == 0) continue; if (values.Count < 1) continue;
// 2) 사분위수/중앙값 계산 var item = CreateBoxPlotItem(ti, values);
double q1 = Percentile(values, 0.25); if (item != null) series.Items.Add(item);
double median = Percentile(values, 0.50); }
double q3 = Percentile(values, 0.75);
double iqr = q3 - q1;
// 3) 수염(윗/아랫 경계)과 이상치(Tukey 1.5*IQR) if (series.Items.Count > 0)
double lowerFence = q1 - 1.5 * iqr; Model.Series.Add(series);
double upperFence = q3 + 1.5 * iqr; }
var inliers = values.Where(v => v >= lowerFence && v <= upperFence).ToList();
var outliers = values.Where(v => v < lowerFence || v > upperFence).ToList();
double lowerWhisker = inliers.Count > 0 ? inliers.First() : values.First();
double upperWhisker = inliers.Count > 0 ? inliers.Last() : values.Last();
// 4) 아이템 추가 (position = X축 인덱스) Model.Legends.Clear();
string label = xAxisKind.Name switch Model.IsLegendVisible = true;
Model.Legends.Add(new Legend
{ {
"수조" or "Tank" => $"수조 {key}", LegendPlacement = LegendPlacement.Outside,
"시간" or "RecordedTime" => FormatBucket(FloorToBucket(data.First().RecordedTime, bucket), bucket), LegendPosition = LegendPosition.RightTop,
_ => key LegendOrientation = LegendOrientation.Vertical,
}; LegendTitle = "수조",
xAxis.Labels.Add(label); TextColor = OxyColors.Black
int position = xAxis.Labels.Count - 1; });
var item = new BoxPlotItem(position, lowerWhisker, q1, median, q3, upperWhisker);
foreach (var o in outliers) item.Outliers.Add(o);
series.Items.Add(item);
}
Model.Series.Add(series);
Model.IsLegendVisible = false;
Model.InvalidatePlot(true); Model.InvalidatePlot(true);
} }
@ -343,42 +365,42 @@ namespace SmartAquaViewer.ViewModel
MarkerFill = OxyColors.DeepSkyBlue MarkerFill = OxyColors.DeepSkyBlue
}; };
foreach (var row in rows) //foreach (var row in rows)
{ //{
double x = ResolveTank(row, xAxisField.Name) ?? double.NaN; // double x = ResolveTank(row, xAxisField.Name) ?? double.NaN;
double y = ResolveTank(row, yAxisField.Name) ?? double.NaN; // double y = ResolveTank(row, yAxisField.Name) ?? double.NaN;
scatterSeries.Points.Add(new ScatterPoint(x, y)); // scatterSeries.Points.Add(new ScatterPoint(x, y));
} //}
Model.Series.Add(scatterSeries); Model.Series.Add(scatterSeries);
if (showRegression && rows.Count > 1) //if (showRegression && rows.Count > 1)
{ //{
var points = rows.Select(r // var points = rows.Select(r
=> new DataPoint( // => new DataPoint(
ResolveTank(r, xAxisField.Name) ?? double.NaN, // ResolveTank(r, xAxisField.Name) ?? double.NaN,
ResolveTank(r, yAxisField.Name) ?? double.NaN)) // ResolveTank(r, yAxisField.Name) ?? double.NaN))
.ToList(); // .ToList();
var regression = LinearRegression(points); // var regression = LinearRegression(points);
if (regression != null) // if (regression != null)
{ // {
var lineSeries = new LineSeries // var lineSeries = new LineSeries
{ // {
Title = "Regression", // Title = "Regression",
Color = OxyColors.Red, // Color = OxyColors.Red,
StrokeThickness = 2 // StrokeThickness = 2
}; // };
// 최소/최대 구간으로 선 그리기 // // 최소/최대 구간으로 선 그리기
double minX = points.Min(p => p.X); // double minX = points.Min(p => p.X);
double maxX = points.Max(p => p.X); // double maxX = points.Max(p => p.X);
lineSeries.Points.Add(new DataPoint(minX, regression.Value.Intercept + regression.Value.Slope * minX)); // lineSeries.Points.Add(new DataPoint(minX, regression.Value.Intercept + regression.Value.Slope * minX));
lineSeries.Points.Add(new DataPoint(maxX, regression.Value.Intercept + regression.Value.Slope * maxX)); // lineSeries.Points.Add(new DataPoint(maxX, regression.Value.Intercept + regression.Value.Slope * maxX));
Model.Series.Add(lineSeries); // Model.Series.Add(lineSeries);
} // }
} //}
Model.InvalidatePlot(true); Model.InvalidatePlot(true);
} }
@ -619,6 +641,25 @@ namespace SmartAquaViewer.ViewModel
Model.InvalidatePlot(true); Model.InvalidatePlot(true);
} }
private BoxPlotItem CreateBoxPlotItem(int xIndex, List<double> values)
{
values.Sort();
int n = values.Count;
if (n == 0) return null;
double q1 = values[(int)(0.25 * (n - 1))];
double q3 = values[(int)(0.75 * (n - 1))];
double median = values[(int)(0.5 * (n - 1))];
double iqr = q3 - q1;
double lower = values.Where(v => v >= q1 - 1.5 * iqr).DefaultIfEmpty(q1).Min();
double upper = values.Where(v => v <= q3 + 1.5 * iqr).DefaultIfEmpty(q3).Max();
return new BoxPlotItem(xIndex, lower, q1, median, q3, upper)
{
Outliers = values.Where(v => v < lower || v > upper).ToList()
};
}
private DateTime FloorToBucket(DateTime dt, TimeSpan bucket) private DateTime FloorToBucket(DateTime dt, TimeSpan bucket)
{ {
long ticks = bucket.Ticks; long ticks = bucket.Ticks;
@ -626,16 +667,15 @@ namespace SmartAquaViewer.ViewModel
return new DateTime(floored, dt.Kind); return new DateTime(floored, dt.Kind);
} }
private double? ResolveTank(WaterQualityVO vo, string fieldName) private double? ResolveTank(WaterTank tank, string fieldName)
{ {
return fieldName switch return fieldName switch
{ {
"RecordedTime" => DateTimeAxis.ToDouble(vo.RecordedTime), "DOValue" => tank.DOValue,
"Tank.DOValue" => vo.Tank.DOValue, "PH" => tank.PH,
"Tank.PH" => vo.Tank.PH, "ORP" => tank.ORP,
"Tank.ORP" => vo.Tank.ORP, "Temperature" => tank.Temperature,
"Tank.Temperature" => vo.Tank.Temperature, "FlowRate" => tank.FlowRate,
"Tank.FlowRate" => vo.Tank.FlowRate,
_ => null _ => null
}; };
} }
@ -680,13 +720,18 @@ namespace SmartAquaViewer.ViewModel
}; };
} }
private string ResolveTankOrTime(WaterQualityVO vo, string fieldName, TimeSpan? bucket = null) private string ResolveTankOrTime(WaterQualityVO data, string xAxisKind, TimeSpan bucket)
{ {
return fieldName switch int? tankId = data.Tanks?.FirstOrDefault().Number; // 분리된 구조이므로 항상 하나만 존재해야 함
return xAxisKind switch
{ {
"수조" or "Tank.Number" => vo.Tank.Number.ToString(), "수조" or "Tank" => tankId?.ToString() ?? "Unknown",
"시간" or "RecordedTime" => FormatBucket(FloorToBucket(vo.RecordedTime, (TimeSpan)bucket), (TimeSpan)bucket),
_ => "" "시간" or "RecordedTime" =>
$"{FloorToBucket(data.RecordedTime, bucket):yyyy-MM-dd HH:mm} | Tank {tankId}",
_ => "Unknown"
}; };
} }

@ -39,15 +39,15 @@ namespace SmartAquaViewer.ViewModel
SelectedViewModel = new MonitoringViewModel(); // Default view SelectedViewModel = new MonitoringViewModel(); // Default view
//더미데이터 생성 및 파일로 저장 //더미데이터 생성 및 파일로 저장
//for (int i = 1; i <= 10; i++) for (int i = 1; i <= 10; i++)
//{ {
// DateTime date = new(2025, 8, i); DateTime date = new(2025, 8, i);
// var dataList = WaterQualityVO.GetSampleData(date, date, 24); var dataList = WaterQualityVO.GetSampleData(date, date, 24);
// var jsonStr = JsonConvert.SerializeObject(dataList, Formatting.Indented); var jsonStr = JsonConvert.SerializeObject(dataList, Formatting.Indented);
// string fileName = date.ToString("yyyy-MM-dd") + ".json"; string fileName = date.ToString("yyyy-MM-dd") + ".json";
// File.WriteAllText(fileName, jsonStr); File.WriteAllText(fileName, jsonStr);
//} }
} }
private void SwapView(object obj) private void SwapView(object obj)

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -25,6 +26,18 @@ namespace SmartAquaViewer.ViewModel
public StepFieldKind Kind { get; init; } public StepFieldKind Kind { get; init; }
} }
public class TanksByTime
{
public DateTime RecordedTime { get; }
public List<WaterTank> Tanks { get; } = new();
public TanksByTime(DateTime time, List<WaterTank> tanks)
{
RecordedTime = time;
Tanks = tanks;
}
}
public class MonitoringViewModel : INotifyPropertyChanged public class MonitoringViewModel : INotifyPropertyChanged
{ {
#region Properties #region Properties
@ -33,7 +46,13 @@ namespace SmartAquaViewer.ViewModel
public ObservableCollection<GraphType> GraphTypes { get; } public ObservableCollection<GraphType> GraphTypes { get; }
public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityList { get; } public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityList { get; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> TankGroups { get; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> TankGroups { get; set; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> SelectedWaterTanks { get; } = new();
public ObservableCollection<TanksByTime> TanksByTimes { get; } = new();
private ObservableCollection<int> SelectedTankNumbers { get; } = new();
private MonitorTab _selectedTab; private MonitorTab _selectedTab;
public MonitorTab SelectedTab public MonitorTab SelectedTab
@ -122,8 +141,6 @@ namespace SmartAquaViewer.ViewModel
get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE); get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE);
} }
public Dictionary<int, ObservableCollection<WaterQualityVO>> SelectedWaterTanks { get; } = new();
private bool _isOpenMode; private bool _isOpenMode;
public bool IsOpenMode public bool IsOpenMode
{ {
@ -201,6 +218,9 @@ namespace SmartAquaViewer.ViewModel
private bool _showMarkers; // Line private bool _showMarkers; // Line
public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } } public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } }
private bool _showLegends;
public bool ShowLegends { get => _showLegends; set { _showLegends = value; OnPropertyChanged(); } }
private bool _useSmoothing; // Line private bool _useSmoothing; // Line
public bool UseSmoothing { get => _useSmoothing; set { _useSmoothing = value; OnPropertyChanged(); } } public bool UseSmoothing { get => _useSmoothing; set { _useSmoothing = value; OnPropertyChanged(); } }
@ -246,14 +266,9 @@ namespace SmartAquaViewer.ViewModel
//Datas.Instance.SetWaterQualityVO(WaterQualityList); //Datas.Instance.SetWaterQualityVO(WaterQualityList);
WaterQualityList = Datas.Instance.WaterQualityView; WaterQualityList = Datas.Instance.WaterQualityView;
TankGroups = WaterQualityList ((INotifyCollectionChanged)WaterQualityList).CollectionChanged += OnWaterQualityChanged;
.GroupBy(x => x.Tank.Number) // 또는 x.Tank.Num
.OrderBy(g => g.Key) RebuildAllGroups();
.ToDictionary(
g => g.Key,
g => new ObservableCollection<WaterQualityVO>(
g.OrderBy(r => r.RecordedTime))
);
GraphTypes = []; GraphTypes = [];
SelectedTab = MonitorTab.Tank; // Default system SelectedTab = MonitorTab.Tank; // Default system
@ -266,23 +281,48 @@ namespace SmartAquaViewer.ViewModel
RebuildFieldCandidates(); RebuildFieldCandidates();
} }
private void OnWaterQualityChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RebuildAllGroups(); // 변경될 때마다 그룹 재구성
}
private void RebuildAllGroups()
{
var grouped = WaterQualityList
.GroupBy(x => x.Tank.Number) // 또는 x.Tank.Num
.OrderBy(g => g.Key)
.ToDictionary(
g => g.Key,
g => new ObservableCollection<WaterQualityVO>(
g.OrderBy(r => r.RecordedTime))
);
TankGroups = grouped;
OnPropertyChanged(nameof(TankGroups));
// TankGroups를 깔끔하게 다시 구성
TanksByTimes.Clear();
foreach (var w in WaterQualityList)
{
var tbt = new TanksByTime(w.RecordedTime, w.Tanks);
TanksByTimes.Add(tbt);
}
}
private void DrawGraph(object obj) private void DrawGraph(object obj)
{ {
switch (SelectedGraphType) switch (SelectedGraphType)
{ {
case GraphType.LINE: case GraphType.LINE:
var xField = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null; if (SelectedTab.Equals(MonitorTab.Tank))
var yField = SelectedYField; SetGraphData_Line_Tank();
var isMarker = ShowMarkers; else
if (SelectedTab.Equals(MonitorTab.Tank)) SetGraphData_Line_Tank(xField, yField, isMarker); GraphControlVM.SetDefaultLineGraph(
else GraphControlVM.SetDefaultLineGraph(WaterQualityList.ToList(), SelectedTab, xField, yField, isMarker); WaterQualityList.ToList(), SelectedTab, SelectedXField, SelectedYField, ShowMarkers);
break; break;
case GraphType.BOX: case GraphType.BOX:
var xFieldBox = SelectedXField; var xFieldBox = SelectedXField;
var dataFieldBox = SelectedYField; SetGraphData_Box_Tank();
var boxWidth = BoxWidth;
var boxTimeSpan = TimeSpan.FromHours(BoxTimeSpan);
GraphControlVM.SetBoxPlot(WaterQualityList.ToList(), xFieldBox, dataFieldBox, boxWidth, boxTimeSpan);
break; break;
case GraphType.SCATTER: case GraphType.SCATTER:
var xFieldScatter = SelectedXField; var xFieldScatter = SelectedXField;
@ -306,13 +346,36 @@ namespace SmartAquaViewer.ViewModel
} }
} }
private void SetGraphData_Line_Tank(FieldItem? xField, FieldItem? yField, bool showMarkers) private void SetGraphData_Line_Tank()
{ {
if (SelectedTab != MonitorTab.Tank) return; if (SelectedTab != MonitorTab.Tank) return;
if (xField?.Name != "RecordedTime" || yField == null) return; if (SelectedYField == null) return;
if (SelectedWaterTanks.Count == 0) return;
var keys = SelectedWaterTanks.Keys.ToList();
var selectedTanks = WaterQualityList
.SelectMany(wq => wq.Tanks.Select(tank => new { Tank = tank, VO = wq }))
.Where(x => keys.Contains(x.Tank.Number))
.GroupBy(x => x.Tank.Number)
.ToDictionary(
g => g.Key,
g => new ObservableCollection<WaterQualityVO>(
g.Select(x => x.VO).OrderBy(vo => vo.RecordedTime))
);
GraphControlVM.SetTankLineGraph(selectedTanks, SelectedXField, SelectedYField, ShowMarkers, ShowLegends);
}
private void SetGraphData_Box_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (SelectedYField == null) return;
var boxTimeSpan = TimeSpan.FromHours(BoxTimeSpan);
var keys = SelectedWaterTanks.Keys.ToList();
GraphControlVM.SetTankLineGraph(SelectedWaterTanks, xField, yField, showMarkers); GraphControlVM.SetBoxPlot(WaterQualityList, keys, SelectedXField, SelectedYField, BoxWidth, boxTimeSpan);
} }
private void SetGraphType() private void SetGraphType()
@ -351,12 +414,12 @@ namespace SmartAquaViewer.ViewModel
if (SelectedTab == MonitorTab.Tank) if (SelectedTab == MonitorTab.Tank)
{ {
AvailableFields.Add(new FieldItem { Name = "Tank.Number", Display = "수조", DataType = typeof(int), Kind = StepFieldKind.Sensor }); AvailableFields.Add(new FieldItem { Name = "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 = "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 = "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 = "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 = "Temperature", Display = "온도 (℃)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.FlowRate", Display = "유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor }); AvailableFields.Add(new FieldItem { Name = "FlowRate", Display = "유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
} }
else if (SelectedTab == MonitorTab.Filter) else if (SelectedTab == MonitorTab.Filter)
{ {
@ -418,10 +481,12 @@ namespace SmartAquaViewer.ViewModel
// X축: 시간 우선 // X축: 시간 우선
foreach (var f in AvailableFields) foreach (var f in AvailableFields)
{ {
if (SelectedGraphType == GraphType.SCATTER && f.Name.Equals("Tank.Number")) continue; if ((SelectedGraphType == GraphType.LINE
|| SelectedGraphType == GraphType.STEP
|| SelectedGraphType == GraphType.SCATTER)
&& f.Name.Equals("Number")) continue;
XFieldCandidates.Add(f); XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.LINE || SelectedGraphType == GraphType.STEP) break; if (SelectedGraphType == GraphType.STEP || SelectedGraphType == GraphType.BOX) break;
if (SelectedGraphType == GraphType.BOX && f.Name.Equals("Tank.Number")) break;
} }
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime)) SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault(); ?? AvailableFields.FirstOrDefault();

Loading…
Cancel
Save