Skip to content

Commit

Permalink
🎨 Add vertical grid lines to charts.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Apr 22, 2024
1 parent 655f1cd commit 2a9ab24
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 17 deletions.
35 changes: 29 additions & 6 deletions Exo.Settings.Ui/Controls/LineChart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ 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 = VerticalGridLinesPathPartName, 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 VerticalGridLinesPathPartName = "PART_VerticalGridLinesPath";
private const string MinMaxPathPartName = "PART_MinMaxLinesPath";

public ITimeSeries? Series
Expand Down Expand Up @@ -93,6 +95,14 @@ public Brush HorizontalGridStroke

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

public Brush VerticalGridStroke
{
get => (Brush)GetValue(VerticalGridStrokeProperty);
set => SetValue(VerticalGridStrokeProperty, value);
}

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

public Brush MinMaxLineStroke
{
get => (Brush)GetValue(MinMaxLineStrokeProperty);
Expand All @@ -104,6 +114,7 @@ public Brush MinMaxLineStroke
private Path? _strokePath;
private Path? _fillPath;
private Path? _horizontalGridLinesPath;
private Path? _verticalGridLinesPath;
private Path? _minMaxLinesPath;
private Grid? _layoutGrid;
private readonly EventHandler _seriesDataChanged;
Expand Down Expand Up @@ -134,6 +145,7 @@ protected override void OnApplyTemplate()
_strokePath = GetTemplateChild(StrokePathPartName) as Path;
_fillPath = GetTemplateChild(FillPathPartName) as Path;
_horizontalGridLinesPath = GetTemplateChild(HorizontalGridLinesPathPartName) as Path;
_verticalGridLinesPath = GetTemplateChild(VerticalGridLinesPathPartName) as Path;
_minMaxLinesPath = GetTemplateChild(MinMaxPathPartName) as Path;
_layoutGrid = GetTemplateChild(LayoutGridPartName) as Grid;
AttachParts();
Expand All @@ -145,6 +157,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 (_verticalGridLinesPath is not null) _verticalGridLinesPath.Data = null;
if (_minMaxLinesPath is not null) _minMaxLinesPath.Data = null;
}

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

private (PathGeometry Stroke, PathGeometry Fill, GeometryGroup HorizontalGridLines, GeometryGroup MinMaxLines) GenerateCurves(ITimeSeries series, double minValue, double maxValue, double outputWidth, double outputHeight)
private (PathGeometry Stroke, PathGeometry Fill, GeometryGroup HorizontalGridLines, GeometryGroup VerticalGridLines, 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 @@ -200,11 +214,11 @@ 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, tickSpacing) = NiceScale.Compute(minValue, maxValue);
var (scaleMin, scaleMax, tickSpacingY) = NiceScale.Compute(minValue, maxValue);

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

Expand All @@ -229,14 +243,23 @@ private void RefreshChart()
fillFigure.Segments.Add(new LineSegment() { Point = new(outputAmplitudeX, fillFigure.StartPoint.Y) });

var horizontalGridLines = new GeometryGroup();
var verticalGridLines = new GeometryGroup();

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

// NB: Hardcode logic to display exactly 10 ticks, as we assume that datapoints are evenly spaced on a fixed time scale.
var (tickSpacingX, tickOffsetX) = Math.DivRem((uint)series.Length, 10);
for (int i = 0; i <= 10; i++)
{
double x = (int)(tickOffsetX + i * tickSpacingX) * outputAmplitudeX / scaleAmplitudeX;
verticalGridLines.Children.Add(new LineGeometry() { StartPoint = new(x, 0), EndPoint = new(x, outputAmplitudeY) });
}

var minMaxLines = new GeometryGroup();

{
Expand All @@ -252,7 +275,7 @@ private void RefreshChart()
}
}

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

protected override Size MeasureOverride(Size availableSize) => availableSize;
Expand Down
32 changes: 22 additions & 10 deletions Exo.Settings.Ui/Controls/LineChart.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Exo.Settings.Ui.Controls">
<Brush x:Key="LineChartBorderBrush">Gray</Brush>
<Thickness x:Key="LineChartBorderThickness">1</Thickness>
<Brush x:Key="LineChartBackground">White</Brush>
<Brush x:Key="LineChartGridStroke">LightGray</Brush>
<Brush x:Key="LineChartStroke">Black</Brush>
<Thickness x:Key="LineChartStrokeThickness">1</Thickness>
<PenLineJoin x:Key="LineChartStrokeLineJoin">Round</PenLineJoin>
<Brush x:Key="LineMinMaxLineStroke">Red</Brush>
<Brush x:Key="LineChartAreaFill">Black</Brush>
<x:Double x:Key="LineChartAreaOpacity">0.75</x:Double>
<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="MinMaxLineStroke" Value="Red" />
<Setter Property="AreaFill" Value="Black" />
<Setter Property="AreaOpacity" Value="0.75" />
<Setter Property="BorderBrush" Value="{ThemeResource LineChartBorderBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource LineChartBorderThickness}" />
<Setter Property="Background" Value="{ThemeResource LineChartBackground}" />
<Setter Property="HorizontalGridStroke" Value="{ThemeResource LineChartGridStroke}" />
<Setter Property="VerticalGridStroke" Value="{ThemeResource LineChartGridStroke}" />
<Setter Property="Stroke" Value="{ThemeResource LineChartStroke}" />
<Setter Property="StrokeThickness" Value="{ThemeResource LineChartStrokeThickness}" />
<Setter Property="StrokeLineJoin" Value="{ThemeResource LineChartStrokeLineJoin}" />
<Setter Property="MinMaxLineStroke" Value="{ThemeResource LineMinMaxLineStroke}" />
<Setter Property="AreaFill" Value="{ThemeResource LineChartAreaFill}" />
<Setter Property="AreaOpacity" Value="{ThemeResource LineChartAreaOpacity}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:LineChart">
Expand All @@ -25,6 +36,7 @@
BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="PART_LayoutGrid">
<Path x:Name="PART_HorizontalGridLinesPath" Stroke="{TemplateBinding HorizontalGridStroke}" />
<Path x:Name="PART_VerticalGridLinesPath" 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}" />
Expand Down
8 changes: 7 additions & 1 deletion Exo.Settings.Ui/SensorsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@
HorizontalGridStroke="{ThemeResource AccentAcrylicBackgroundFillColorDefaultBrush}"
ScaleYMinimum="{Binding ScaleMinimumValue, Mode=OneWay}"
ScaleYMaximum="{Binding ScaleMaximumValue, Mode=OneWay}" />
<TextBlock Grid.Row="1" Text="{Binding LiveDetails.CurrentValue}" Foreground="{ThemeResource TextOnAccentFillColorSelectedTextBrush}" 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

0 comments on commit 2a9ab24

Please sign in to comment.