Skip to content

Commit

Permalink
🎨 Add grid lines to charts and fix the nice scales.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Apr 19, 2024
1 parent 85d281f commit 9f5f6e9
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 18 deletions.
62 changes: 45 additions & 17 deletions Exo.Settings.Ui/Controls/LineChart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ namespace Exo.Settings.Ui.Controls;
[TemplatePart(Name = LayoutGridPartName, Type = typeof(Grid))]
[TemplatePart(Name = StrokePathPartName, Type = typeof(Path))]
[TemplatePart(Name = FillPathPartName, Type = typeof(Path))]
[TemplatePart(Name = HorizontalGridLinesPathPartName, 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";

public ITimeSeries? Series
{
get => (ITimeSeries)GetValue(SeriesProperty);
set => SetValue(SeriesProperty, value);
}

public static readonly DependencyProperty SeriesProperty = DependencyProperty.Register("Series", typeof(ITimeSeries), typeof(LineChart), new PropertyMetadata(null, OnSeriesChanged));
public static readonly DependencyProperty SeriesProperty = DependencyProperty.Register(nameof(Series), typeof(ITimeSeries), typeof(LineChart), new PropertyMetadata(null, OnSeriesChanged));

// TODO: Should be nullable once the WinUI bug is fixed.
public double ScaleYMinimum
Expand All @@ -30,7 +32,7 @@ public double ScaleYMinimum
set => SetValue(ScaleYMinimumProperty, value);
}

public static readonly DependencyProperty ScaleYMinimumProperty = DependencyProperty.Register("ScaleYMinimum", typeof(double), typeof(LineChart), new PropertyMetadata(double.PositiveInfinity, OnScaleChanged));
public static readonly DependencyProperty ScaleYMinimumProperty = DependencyProperty.Register(nameof(ScaleYMinimum), typeof(double), typeof(LineChart), new PropertyMetadata(double.PositiveInfinity, OnScaleChanged));

// TODO: Should be nullable once the WinUI bug is fixed.
public double ScaleYMaximum
Expand All @@ -39,42 +41,59 @@ public double ScaleYMaximum
set => SetValue(ScaleYMaximumProperty, value);
}

public static readonly DependencyProperty ScaleYMaximumProperty = DependencyProperty.Register("ScaleYMaximum", typeof(double), typeof(LineChart), new PropertyMetadata(double.NegativeInfinity, OnScaleChanged));
public static readonly DependencyProperty ScaleYMaximumProperty = DependencyProperty.Register(nameof(ScaleYMaximum), typeof(double), typeof(LineChart), new PropertyMetadata(double.NegativeInfinity, OnScaleChanged));

public Brush AreaFill
{
get => (Brush)GetValue(AreaFillProperty);
set => SetValue(AreaFillProperty, value);
}

public static readonly DependencyProperty AreaFillProperty = DependencyProperty.Register("AreaFill", typeof(Brush), typeof(LineChart), new PropertyMetadata(new SolidColorBrush()));
public static readonly DependencyProperty AreaFillProperty = DependencyProperty.Register(nameof(AreaFill), typeof(Brush), typeof(LineChart), new PropertyMetadata(new SolidColorBrush()));

public double AreaOpacity
{
get => (double)GetValue(AreaOpacityProperty);
set => SetValue(AreaOpacityProperty, value);
}

public static readonly DependencyProperty AreaOpacityProperty = DependencyProperty.Register("AreaOpacity", typeof(double), typeof(LineChart), new PropertyMetadata(1d));
public static readonly DependencyProperty AreaOpacityProperty = DependencyProperty.Register(nameof(AreaOpacity), typeof(double), typeof(LineChart), new PropertyMetadata(1d));

public Brush Stroke
{
get => (Brush)GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}

public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register("Stroke", typeof(Brush), typeof(LineChart), new PropertyMetadata(new SolidColorBrush()));
public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register(nameof(Stroke), typeof(Brush), typeof(LineChart), new PropertyMetadata(new SolidColorBrush()));

public double StrokeThickness
{
get => (double)GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}

public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(nameof(StrokeThickness), typeof(double), typeof(LineChart), new PropertyMetadata(1d));

public PenLineJoin StrokeLineJoin
{
get => (PenLineJoin)GetValue(StrokeLineJoinProperty);
set => SetValue(StrokeLineJoinProperty, value);
}

public static readonly DependencyProperty StrokeLineJoinProperty = DependencyProperty.Register("StrokeLineJoin", typeof(PenLineJoin), typeof(LineChart), new PropertyMetadata(PenLineJoin.Round));
public static readonly DependencyProperty StrokeLineJoinProperty = DependencyProperty.Register(nameof(StrokeLineJoin), typeof(PenLineJoin), typeof(LineChart), new PropertyMetadata(PenLineJoin.Round));

public Brush HorizontalGridStroke
{
get => (Brush)GetValue(HorizontalGridStrokeProperty);
set => SetValue(HorizontalGridStrokeProperty, value);
}

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

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

Expand Down Expand Up @@ -103,6 +122,7 @@ protected override void OnApplyTemplate()
DetachParts();
_strokePath = GetTemplateChild(StrokePathPartName) as Path;
_fillPath = GetTemplateChild(FillPathPartName) as Path;
_horizontalGridLinesPath = GetTemplateChild(HorizontalGridLinesPathPartName) as Path;
_layoutGrid = GetTemplateChild(LayoutGridPartName) as Grid;
AttachParts();
RefreshChart();
Expand All @@ -112,12 +132,11 @@ 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;
}

private void AttachParts()
{
if (_strokePath is not null) _strokePath.Data = null;
if (_fillPath is not null) _fillPath.Data = null;
}

private void RefreshChart()
Expand All @@ -129,7 +148,7 @@ private void RefreshChart()
}
else
{
var (stroke, fill) = GenerateCurves
var (stroke, fill, horizontalGridLines) = GenerateCurves
(
Series,
ScaleYMinimum,
Expand All @@ -139,10 +158,11 @@ private void RefreshChart()
);
if (_strokePath is { }) _strokePath.Data = stroke;
if (_fillPath is { }) _fillPath.Data = fill;
if (_horizontalGridLinesPath is { }) _horizontalGridLinesPath.Data = horizontalGridLines;
}
}

private (PathGeometry Stroke, PathGeometry Fill) GenerateCurves(ITimeSeries series, double minValue, double maxValue, double outputWidth, double outputHeight)
private (PathGeometry Stroke, PathGeometry Fill, GeometryGroup HorizontalGridLines) 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 @@ -161,34 +181,42 @@ private void RefreshChart()
// Force the chart to not be fully empty if the min and max are both zero. (result of previous adjustments)
if (minValue == maxValue) maxValue = 1;

var (scaleMin, scaleMax, _) = NiceScale.Compute(minValue, maxValue);
var (scaleMin, scaleMax, tickSpacing) = NiceScale.Compute(minValue, maxValue);

double scaleAmplitudeX = series.Length - 1;
double scaleAmplitudeY = maxValue - minValue;
double scaleAmplitudeY = scaleMax - scaleMin;
double outputAmplitudeX = outputWidth;
double outputAmplitudeY = outputHeight;

var outlineFigure = new PathFigure();
var fillFigure = new PathFigure();

fillFigure.StartPoint = new(0, outputAmplitudeY - -minValue * outputAmplitudeY / scaleAmplitudeY);
fillFigure.StartPoint = new(0, outputAmplitudeY - -scaleMin * outputAmplitudeY / scaleAmplitudeY);

var point = new Point(0, outputAmplitudeY - (series[0] - minValue) * outputAmplitudeY / scaleAmplitudeY);
var point = new Point(0, outputAmplitudeY - (series[0] - scaleMin) * outputAmplitudeY / scaleAmplitudeY);
outlineFigure.StartPoint = point;
fillFigure.Segments.Add(new LineSegment { Point = point });
for (int j = 1; j < series.Length; j++)
{
double value = series[j];
double x = j * outputAmplitudeX / scaleAmplitudeX;
double y = outputAmplitudeY - (value - minValue) * outputAmplitudeY / scaleAmplitudeY;
double y = outputAmplitudeY - (value - scaleMin) * outputAmplitudeY / scaleAmplitudeY;
point = new Point(x, y);
outlineFigure.Segments.Add(new LineSegment() { Point = point });
fillFigure.Segments.Add(new LineSegment() { Point = point });
}

fillFigure.Segments.Add(new LineSegment() { Point = new(outputAmplitudeX, fillFigure.StartPoint.Y) });

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

for (double lineY = scaleMin + tickSpacing; lineY < scaleMax; 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);
}

protected override Size MeasureOverride(Size availableSize) => availableSize;
Expand Down
12 changes: 11 additions & 1 deletion Exo.Settings.Ui/Controls/LineChart.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Exo.Settings.Ui.Controls">
<Style TargetType="local:LineChart">
<Setter Property="BorderBrush" Value="Gray" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Background" Value="White" />
<Setter Property="HorizontalGridStroke" Value="LightGray" />
<Setter Property="Stroke" Value="Black" />
<Setter Property="StrokeThickness" Value="1" />
<Setter Property="StrokeLineJoin" Value="Round" />
<Setter Property="AreaFill" Value="Black" />
<Setter Property="AreaOpacity" Value="0.75" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:LineChart">
Expand All @@ -14,8 +23,9 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="PART_LayoutGrid">
<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}" StrokeLineJoin="{TemplateBinding StrokeLineJoin}" />
<Path x:Name="PART_StrokePath" Stroke="{TemplateBinding Stroke}" StrokeThickness="{TemplateBinding StrokeThickness}" StrokeLineJoin="{TemplateBinding StrokeLineJoin}" />
</Grid>
</Border>
</ControlTemplate>
Expand Down
2 changes: 2 additions & 0 deletions Exo.Settings.Ui/SensorsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@
Background="{ThemeResource AccentAcrylicBackgroundFillColorBaseBrush}"
HorizontalAlignment="Center"
Stroke="{ThemeResource AccentFillColorDefaultBrush}"
StrokeThickness="1"
StrokeLineJoin="Round"
AreaFill="{ThemeResource AccentFillColorDefaultBrush}"
AreaOpacity="0.5"
Series="{Binding LiveDetails.History}"
BorderBrush="{StaticResource TextFillColorSecondaryBrush}"
BorderThickness="1"
HorizontalGridStroke="{ThemeResource AccentFillColorSelectedTextBackgroundBrush}"
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" />
Expand Down

0 comments on commit 9f5f6e9

Please sign in to comment.