Skip to content

Commit

Permalink
🎨 Add min/max lines to charts, and tweak chart colors.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Apr 21, 2024
1 parent 553838b commit 655f1cd
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 8 deletions.
5 changes: 4 additions & 1 deletion Exo.Settings.Ui/Controls/ITimeSeries.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Exo.Settings.Ui.Controls;
namespace Exo.Settings.Ui.Controls;

internal interface ITimeSeries
{
Expand All @@ -7,5 +7,8 @@ internal interface ITimeSeries
int Length { get; }
double this[int index] { get; }

double? MaximumReachedValue { get; }
double? MinimumReachedValue { get; }

event EventHandler Changed;
}
44 changes: 40 additions & 4 deletions Exo.Settings.Ui/Controls/LineChart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ namespace Exo.Settings.Ui.Controls;
[TemplatePart(Name = StrokePathPartName, Type = typeof(Path))]
[TemplatePart(Name = FillPathPartName, Type = typeof(Path))]
[TemplatePart(Name = HorizontalGridLinesPathPartName, Type = typeof(Path))]
[TemplatePart(Name = MinMaxPathPartName, Type = typeof(Path))]
internal class LineChart : Control
{
private const string LayoutGridPartName = "PART_LayoutGrid";
private const string StrokePathPartName = "PART_StrokePath";
private const string FillPathPartName = "PART_FillPath";
private const string HorizontalGridLinesPathPartName = "PART_HorizontalGridLinesPath";
private const string MinMaxPathPartName = "PART_MinMaxLinesPath";

public ITimeSeries? Series
{
Expand Down Expand Up @@ -91,9 +93,18 @@ public Brush HorizontalGridStroke

public static readonly DependencyProperty HorizontalGridStrokeProperty = DependencyProperty.Register(nameof(HorizontalGridStroke), typeof(Brush), typeof(LineChart), new PropertyMetadata(new SolidColorBrush()));

public Brush MinMaxLineStroke
{
get => (Brush)GetValue(MinMaxLineStrokeProperty);
set => SetValue(MinMaxLineStrokeProperty, value);
}

public static readonly DependencyProperty MinMaxLineStrokeProperty = DependencyProperty.Register(nameof(MinMaxLineStroke), typeof(Brush), typeof(LineChart), new PropertyMetadata(new SolidColorBrush()));

private Path? _strokePath;
private Path? _fillPath;
private Path? _horizontalGridLinesPath;
private Path? _minMaxLinesPath;
private Grid? _layoutGrid;
private readonly EventHandler _seriesDataChanged;

Expand Down Expand Up @@ -123,6 +134,7 @@ protected override void OnApplyTemplate()
_strokePath = GetTemplateChild(StrokePathPartName) as Path;
_fillPath = GetTemplateChild(FillPathPartName) as Path;
_horizontalGridLinesPath = GetTemplateChild(HorizontalGridLinesPathPartName) as Path;
_minMaxLinesPath = GetTemplateChild(MinMaxPathPartName) as Path;
_layoutGrid = GetTemplateChild(LayoutGridPartName) as Grid;
AttachParts();
RefreshChart();
Expand All @@ -133,6 +145,7 @@ private void DetachParts()
if (_strokePath is not null) _strokePath.Data = null;
if (_fillPath is not null) _fillPath.Data = null;
if (_horizontalGridLinesPath is not null) _horizontalGridLinesPath.Data = null;
if (_minMaxLinesPath is not null) _minMaxLinesPath.Data = null;
}

private void AttachParts()
Expand All @@ -148,7 +161,7 @@ private void RefreshChart()
}
else
{
var (stroke, fill, horizontalGridLines) = GenerateCurves
var (stroke, fill, horizontalGridLines, minMaxLines) = GenerateCurves
(
Series,
ScaleYMinimum,
Expand All @@ -159,10 +172,11 @@ private void RefreshChart()
if (_strokePath is { }) _strokePath.Data = stroke;
if (_fillPath is { }) _fillPath.Data = fill;
if (_horizontalGridLinesPath is { }) _horizontalGridLinesPath.Data = horizontalGridLines;
if (_minMaxLinesPath is { }) _minMaxLinesPath.Data = minMaxLines;
}
}

private (PathGeometry Stroke, PathGeometry Fill, GeometryGroup HorizontalGridLines) GenerateCurves(ITimeSeries series, double minValue, double maxValue, double outputWidth, double outputHeight)
private (PathGeometry Stroke, PathGeometry Fill, GeometryGroup HorizontalGridLines, GeometryGroup MinMaxLines) GenerateCurves(ITimeSeries series, double minValue, double maxValue, double outputWidth, double outputHeight)
{
// NB: This is very rough and WIP.
// It should probably be ported to a dedicated chart drawing component afterwards.
Expand All @@ -174,6 +188,11 @@ private void RefreshChart()
maxValue = Math.Max(value, maxValue);
}

{
if (series.MinimumReachedValue is double minReachedValue && minReachedValue < minValue) minValue = minReachedValue;
if (series.MaximumReachedValue is double maxReachedValue && maxReachedValue > maxValue) maxValue = maxReachedValue;
}

// Anchor the scale to zero if necessary.
if (maxValue < 0) maxValue = 0;
if (minValue > 0) minValue = 0;
Expand All @@ -185,6 +204,7 @@ private void RefreshChart()

double scaleAmplitudeX = series.Length - 1;
double scaleAmplitudeY = scaleMax - scaleMin;
int tickCount = (int)(scaleAmplitudeY / tickSpacing) + 1;
double outputAmplitudeX = outputWidth;
double outputAmplitudeY = outputHeight;

Expand All @@ -210,13 +230,29 @@ private void RefreshChart()

var horizontalGridLines = new GeometryGroup();

for (double lineY = scaleMin + tickSpacing; lineY < scaleMax; lineY += tickSpacing)
double lineY = scaleMin;
for (int i = 0; i < tickCount; i++, lineY += tickSpacing)
{
double y = outputAmplitudeY - lineY * outputAmplitudeY / scaleAmplitudeY;
horizontalGridLines.Children.Add(new LineGeometry() { StartPoint = new(0, y), EndPoint = new(outputAmplitudeX, y) });
}

return (new PathGeometry() { Figures = { outlineFigure } }, new PathGeometry() { Figures = { fillFigure } }, horizontalGridLines);
var minMaxLines = new GeometryGroup();

{
if (series.MinimumReachedValue is double minReachedValue)
{
double y = outputAmplitudeY - (minReachedValue - scaleMin) * outputAmplitudeY / scaleAmplitudeY;
minMaxLines.Children.Add(new LineGeometry() { StartPoint = new(0, y), EndPoint = new(outputAmplitudeX, y) });
}
if (series.MaximumReachedValue is double maxReachedValue)
{
double y = outputAmplitudeY - (maxReachedValue - scaleMin) * outputAmplitudeY / scaleAmplitudeY;
minMaxLines.Children.Add(new LineGeometry() { StartPoint = new(0, y), EndPoint = new(outputAmplitudeX, y) });
}
}

return (new PathGeometry() { Figures = { outlineFigure } }, new PathGeometry() { Figures = { fillFigure } }, horizontalGridLines, minMaxLines);
}

protected override Size MeasureOverride(Size availableSize) => availableSize;
Expand Down
2 changes: 2 additions & 0 deletions Exo.Settings.Ui/Controls/LineChart.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<Setter Property="Stroke" Value="Black" />
<Setter Property="StrokeThickness" Value="1" />
<Setter Property="StrokeLineJoin" Value="Round" />
<Setter Property="MinMaxLineStroke" Value="Red" />
<Setter Property="AreaFill" Value="Black" />
<Setter Property="AreaOpacity" Value="0.75" />
<Setter Property="Template">
Expand All @@ -26,6 +27,7 @@
<Path x:Name="PART_HorizontalGridLinesPath" Stroke="{TemplateBinding HorizontalGridStroke}" />
<Path x:Name="PART_FillPath" Fill="{TemplateBinding AreaFill}" Opacity="{TemplateBinding AreaOpacity}" />
<Path x:Name="PART_StrokePath" Stroke="{TemplateBinding Stroke}" StrokeThickness="{TemplateBinding StrokeThickness}" StrokeLineJoin="{TemplateBinding StrokeLineJoin}" />
<Path x:Name="PART_MinMaxLinesPath" Stroke="{TemplateBinding MinMaxLineStroke}" />
</Grid>
</Border>
</ControlTemplate>
Expand Down
7 changes: 4 additions & 3 deletions Exo.Settings.Ui/SensorsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@
Stroke="{ThemeResource AccentFillColorDefaultBrush}"
StrokeThickness="1"
StrokeLineJoin="Round"
MinMaxLineStroke="{ThemeResource AccentTextFillColorPrimaryBrush}"
AreaFill="{ThemeResource AccentFillColorDefaultBrush}"
AreaOpacity="0.5"
Series="{Binding LiveDetails.History}"
BorderBrush="{StaticResource TextFillColorSecondaryBrush}"
BorderBrush="{StaticResource ControlStrongStrokeColorDefaultBrush}"
BorderThickness="1"
HorizontalGridStroke="{ThemeResource AccentFillColorSelectedTextBackgroundBrush}"
HorizontalGridStroke="{ThemeResource AccentAcrylicBackgroundFillColorDefaultBrush}"
ScaleYMinimum="{Binding ScaleMinimumValue, Mode=OneWay}"
ScaleYMaximum="{Binding ScaleMaximumValue, Mode=OneWay}" />
<TextBlock Grid.Row="1" Text="{Binding LiveDetails.CurrentValue}" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="0,0,8,6" />
<TextBlock Grid.Row="1" Text="{Binding LiveDetails.CurrentValue}" Foreground="{ThemeResource TextOnAccentFillColorSelectedTextBrush}" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="0,0,8,6" />
</Grid>
</ItemContainer>
</DataTemplate>
Expand Down
10 changes: 10 additions & 0 deletions Exo.Settings.Ui/ViewModels/SensorsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,16 @@ public double this[int index]
public event EventHandler? Changed;

public void NotifyChange() => Changed?.Invoke(this, EventArgs.Empty);

public double? MaximumReachedValue => _viewModel._maxValue;
public double? MinimumReachedValue => _viewModel._minValue;
}

private const int WindowSizeInSeconds = 1 * 60;

private double _currentValue;
private double _minValue;
private double _maxValue;
private DateTime _currentValueTime;
private ulong _currentTimestampInSeconds;
private int _currentPointIndex;
Expand All @@ -313,6 +318,9 @@ public double this[int index]

public LiveSensorDetailsViewModel(SensorViewModel sensor)
{
_currentValue = double.NaN;
_minValue = double.PositiveInfinity;
_maxValue = double.NegativeInfinity;
_currentValueTime = DateTime.UtcNow;
_currentTimestampInSeconds = GetTimestamp();
_dataPoints = new double[WindowSizeInSeconds];
Expand Down Expand Up @@ -378,6 +386,8 @@ private async Task WatchAsync(CancellationToken cancellationToken)
_currentValueTime = now;
_currentTimestampInSeconds = currentTimestamp;
_dataPoints[_currentPointIndex] = dataPoint.Value;
if (dataPoint.Value < _minValue) _minValue = dataPoint.Value;
if (dataPoint.Value > _maxValue) _maxValue = dataPoint.Value;
SetValue(ref _currentValue, dataPoint.Value, ChangedProperty.CurrentValue);
History.NotifyChange();
}
Expand Down

0 comments on commit 655f1cd

Please sign in to comment.