├── Images └── ChartControl.png ├── SampleApp ├── Program.cs ├── App.axaml ├── App.axaml.cs ├── SampleApp.csproj ├── Views │ ├── MainWindow.axaml │ └── MainWindow.axaml.cs └── Themes │ └── FluentChartControl.axaml ├── SatialInterfaces ├── README.md ├── Helpers │ ├── ValueConverterHelper.cs │ ├── EnumerableHelper.cs │ ├── SystemHelper.cs │ └── GridHelper.cs ├── Controls │ ├── ChartSelectionChangedEventArgs.cs │ ├── DefaultXValueConverter.cs │ ├── ChartPoint.cs │ ├── ChartControl.axaml │ └── ChartControl.axaml.cs ├── ChartControl.csproj └── Themes │ └── Default.axaml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── ChartControl.sln ├── README.md └── omnisharp.json /Images/ChartControl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satial-interfaces/ChartControl/HEAD/Images/ChartControl.png -------------------------------------------------------------------------------- /SampleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using SampleApp; 3 | 4 | AppBuilder.Configure().UsePlatformDetect().LogToTrace().StartWithClassicDesktopLifetime(args); -------------------------------------------------------------------------------- /SatialInterfaces/README.md: -------------------------------------------------------------------------------- 1 | ChartControl is a chart control (line with markers) for Avalonia. Go to the source repository, see and run the sample app to get to know it. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | *.pdf 7 | nupkg/ 8 | 9 | # User-specific files 10 | .idea/ 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | build/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Oo]ut/ 28 | msbuild.log 29 | msbuild.err 30 | msbuild.wrn 31 | -------------------------------------------------------------------------------- /SampleApp/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SampleApp/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | using SampleApp.Views; 5 | 6 | namespace SampleApp; 7 | 8 | public class App : Application 9 | { 10 | public override void Initialize() => AvaloniaXamlLoader.Load(this); 11 | 12 | public override void OnFrameworkInitializationCompleted() 13 | { 14 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 15 | desktop.MainWindow = new MainWindow(); 16 | base.OnFrameworkInitializationCompleted(); 17 | } 18 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "SampleApp", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceFolder}/SampleApp/bin/Debug/net9.0/SampleApp.dll", 10 | "args": [], 11 | "cwd": "${workspaceFolder}", 12 | "console": "internalConsole", 13 | "stopAtEntry": false 14 | }, 15 | { 16 | "name": ".NET Core Attach", 17 | "type": "coreclr", 18 | "request": "attach" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /SatialInterfaces/Helpers/ValueConverterHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Avalonia.Data.Converters; 3 | 4 | namespace SatialInterfaces.Helpers; 5 | 6 | /// This class contains value converter methods. 7 | internal static class ValueConverterHelper 8 | { 9 | /// 10 | /// Converts the given value to formatted text. 11 | /// 12 | /// Value converter to use. 13 | /// Value to convert. 14 | /// The string. 15 | public static string ConvertValueToText(IValueConverter? valueConverter, object? value) 16 | { 17 | var result = valueConverter != null 18 | ? valueConverter.ConvertBack(value, typeof(string), null, CultureInfo.CurrentCulture)?.ToString() 19 | : value?.ToString(); 20 | return result ?? ""; 21 | } 22 | } -------------------------------------------------------------------------------- /SatialInterfaces/Helpers/EnumerableHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace SatialInterfaces.Helpers; 6 | 7 | /// This class contains IEnumerable methods. 8 | internal static class EnumerableHelper 9 | { 10 | /// 11 | /// Determines whether a sequence contains any elements. 12 | /// 13 | /// The IEnumerable to check for emptiness. 14 | /// true if the source sequence contains any elements; otherwise, false. 15 | public static bool Any(this IEnumerable source) => source.GetEnumerator().MoveNext(); 16 | 17 | /// 18 | /// Creates a list with objects from an enumerable 19 | /// 20 | /// The IEnumerable to convert. 21 | /// The list 22 | public static List ToList(this IEnumerable source) => Enumerable.ToList(source.Cast()); 23 | } -------------------------------------------------------------------------------- /SampleApp/SampleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | enable 5 | Exe 6 | net9.0 7 | copyused 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SatialInterfaces/Helpers/SystemHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace SatialInterfaces.Helpers; 5 | 6 | /// System helper class: it provides methods and extension methods. 7 | internal static class SystemHelper 8 | { 9 | /// 10 | /// Gets a property/field value. 11 | /// 12 | /// The obj to act on. 13 | /// Name of the property/field. 14 | /// The value. 15 | public static object? GetValue(object obj, string memberName) 16 | { 17 | if (string.IsNullOrEmpty(memberName)) return null; 18 | var p = Array.Find(obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly), m => string.Equals(m.Name, memberName, StringComparison.Ordinal)); 19 | if (p != null) 20 | return p.GetValue(obj); 21 | var f = Array.Find(obj.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly), m => string.Equals(m.Name, memberName, StringComparison.Ordinal)); 22 | return f?.GetValue(obj); 23 | } 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Satial Interfaces 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SatialInterfaces/Controls/ChartSelectionChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Interactivity; 2 | 3 | namespace SatialInterfaces.Controls.Chart; 4 | 5 | /// Expansion of the RoutedEventArgs class by providing a selected index;. 6 | public class ChartSelectionChangedEventArgs : RoutedEventArgs 7 | { 8 | /// 9 | /// Initializes a new instance of the ChartSelectionChangedEventArgs class, using the supplied routed event 10 | /// identifier. 11 | /// 12 | /// The routed event identifier for this instance of the ChartSelectionChangedEventArgs class. 13 | public ChartSelectionChangedEventArgs(RoutedEvent routedEvent) : base(routedEvent) 14 | { 15 | } 16 | 17 | /// 18 | /// Property representing the selected index. -1 means not selected. 19 | /// 20 | /// The selected index (zero-based) or -1 if deselected. 21 | public int SelectedIndex { get; set; } 22 | 23 | /// 24 | /// Property representing the selected object. Null means not selected. 25 | /// 26 | /// The selected object or null if deselected. 27 | public object? SelectedItem { get; set; } 28 | } -------------------------------------------------------------------------------- /SatialInterfaces/ChartControl.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Satial Interfaces 4 | Satial Interfaces 5 | ChartControl is a chart control (line with markers) for Avalonia. 6 | latest 7 | enable 8 | ChartControl.Avalonia 9 | MIT 10 | README.md 11 | https://github.com/satial-interfaces/ChartControl 12 | netstandard2.1 13 | 11.2.5 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SatialInterfaces/Helpers/GridHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SatialInterfaces.Helpers; 4 | 5 | /// Grid helper class: it provides methods and extension methods. 6 | internal static class GridHelper 7 | { 8 | public static double GetGridSize(double chartRange, double maximumTicks) 9 | { 10 | var step = chartRange / maximumTicks; 11 | 12 | var mag = Math.Floor(Math.Log10(step)); 13 | var magPow = Math.Pow(10, mag); 14 | 15 | var magMsd = (int)(step / magPow + 0.5); 16 | 17 | magMsd = magMsd switch 18 | { 19 | > 5 => 10, 20 | > 2 => 5, 21 | > 1 => 2, 22 | _ => magMsd 23 | }; 24 | 25 | return magMsd * magPow; 26 | } 27 | 28 | /// 29 | /// Rounds a value up with the given increment. 30 | /// 31 | /// Value. 32 | /// Increment. 33 | /// The rounded value. 34 | public static double RoundUp(double value, double increment) 35 | { 36 | if (value == 0.0) 37 | return value; 38 | var div = value / increment; 39 | if (value < 0) 40 | div -= Math.Truncate(div); 41 | else 42 | div = 1.0 - (div - Math.Truncate(div)); 43 | return value + div * increment; 44 | } 45 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/SampleApp/SampleApp.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/SampleApp/SampleApp.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/SampleApp/SampleApp.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /SatialInterfaces/Controls/DefaultXValueConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data; 4 | using Avalonia.Data.Converters; 5 | using SatialInterfaces.Helpers; 6 | 7 | namespace SatialInterfaces.Controls.Chart; 8 | 9 | public class DefaultXValueConverter : IValueConverter 10 | { 11 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 12 | { 13 | return BindingOperations.DoNothing; 14 | } 15 | 16 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 17 | { 18 | if (targetType != typeof(string) || value == null) 19 | return BindingOperations.DoNothing; 20 | 21 | return string.Format(CultureInfo.CurrentCulture, "{0:F3}", SystemHelper.GetValue(value, "X")); 22 | } 23 | } 24 | 25 | public class DefaultYValueConverter : IValueConverter 26 | { 27 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 28 | { 29 | return BindingOperations.DoNothing; 30 | } 31 | 32 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 33 | { 34 | if (targetType != typeof(string) || value == null) 35 | return BindingOperations.DoNothing; 36 | 37 | return string.Format(CultureInfo.CurrentCulture, "{0:F3}", SystemHelper.GetValue(value, "Y")); 38 | } 39 | } -------------------------------------------------------------------------------- /ChartControl.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChartControl", "SatialInterfaces/ChartControl.csproj", "{44AE23FB-5D6F-43E0-B2B9-7625FEA72BBD}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp", "SampleApp/SampleApp.csproj", "{A41209C6-81B7-411D-9785-00CF8721296A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {44AE23FB-5D6F-43E0-B2B9-7625FEA72BBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {44AE23FB-5D6F-43E0-B2B9-7625FEA72BBD}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {44AE23FB-5D6F-43E0-B2B9-7625FEA72BBD}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {44AE23FB-5D6F-43E0-B2B9-7625FEA72BBD}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {A41209C6-81B7-411D-9785-00CF8721296A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {A41209C6-81B7-411D-9785-00CF8721296A}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {A41209C6-81B7-411D-9785-00CF8721296A}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {A41209C6-81B7-411D-9785-00CF8721296A}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /SampleApp/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChartControl for Avalonia 2 | 3 | This is a chart control (line with markers) for Avalonia. See and run the sample app to get to know it. 4 | 5 | ![ChartControl screenshot](/Images/ChartControl.png) 6 | 7 | ## How to use 8 | 9 | First add the package to your project. Use NuGet to get it: https://www.nuget.org/packages/ChartControl.Avalonia/ 10 | 11 | Or use this command in the Package Manager console to install the package manually 12 | ``` 13 | Install-Package ChartControl.Avalonia 14 | ``` 15 | 16 | Second add a style to your App.axaml (from the sample app) 17 | 18 | ````Xml 19 | 22 | 23 | 24 | 25 | 26 | 27 | ```` 28 | 29 | Or use the default one 30 | 31 | ````Xml 32 | 35 | 36 | 37 | 38 | 39 | 40 | ```` 41 | 42 | Then add the control to your Window.axaml (minimum) 43 | 44 | ````Xml 45 | 48 | 49 | 50 | 51 | 52 | ```` 53 | 54 | It's even better to specify the item template with binding to your view model 55 | 56 | ````Xml 57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ```` 71 | -------------------------------------------------------------------------------- /omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormattingOptions": { 3 | "NewLine": "\n", 4 | "UseTabs": true, 5 | "TabSize": 4, 6 | "IndentationSize": 4, 7 | "SpacingAfterMethodDeclarationName": false, 8 | "SpaceWithinMethodDeclarationParenthesis": false, 9 | "SpaceBetweenEmptyMethodDeclarationParentheses": false, 10 | "SpaceAfterMethodCallName": false, 11 | "SpaceWithinMethodCallParentheses": false, 12 | "SpaceBetweenEmptyMethodCallParentheses": false, 13 | "SpaceAfterControlFlowStatementKeyword": true, 14 | "SpaceWithinExpressionParentheses": false, 15 | "SpaceWithinCastParentheses": false, 16 | "SpaceWithinOtherParentheses": false, 17 | "SpaceAfterCast": false, 18 | "SpacesIgnoreAroundVariableDeclaration": false, 19 | "SpaceBeforeOpenSquareBracket": false, 20 | "SpaceBetweenEmptySquareBrackets": false, 21 | "SpaceWithinSquareBrackets": false, 22 | "SpaceAfterColonInBaseTypeDeclaration": true, 23 | "SpaceAfterComma": true, 24 | "SpaceAfterDot": false, 25 | "SpaceAfterSemicolonsInForStatement": true, 26 | "SpaceBeforeColonInBaseTypeDeclaration": true, 27 | "SpaceBeforeComma": false, 28 | "SpaceBeforeDot": false, 29 | "SpaceBeforeSemicolonsInForStatement": false, 30 | "SpacingAroundBinaryOperator": "single", 31 | "IndentBraces": false, 32 | "IndentBlock": true, 33 | "IndentSwitchSection": true, 34 | "IndentSwitchCaseSection": true, 35 | "IndentSwitchCaseSectionWhenBlock": true, 36 | "LabelPositioning": "oneLess", 37 | "WrappingPreserveSingleLine": true, 38 | "WrappingKeepStatementsOnSingleLine": true, 39 | "NewLinesForBracesInTypes": true, 40 | "NewLinesForBracesInMethods": true, 41 | "NewLinesForBracesInProperties": true, 42 | "NewLinesForBracesInAccessors": true, 43 | "NewLinesForBracesInAnonymousMethods": true, 44 | "NewLinesForBracesInControlBlocks": true, 45 | "NewLinesForBracesInAnonymousTypes": true, 46 | "NewLinesForBracesInObjectCollectionArrayInitializers": true, 47 | "NewLinesForBracesInLambdaExpressionBody": true, 48 | "NewLineForElse": true, 49 | "NewLineForCatch": true, 50 | "NewLineForFinally": true, 51 | "NewLineForMembersInObjectInit": true, 52 | "NewLineForMembersInAnonymousTypes": true, 53 | "NewLineForClausesInQuery": true 54 | } 55 | } -------------------------------------------------------------------------------- /SatialInterfaces/Themes/Default.axaml: -------------------------------------------------------------------------------- 1 | 4 | 18 | 22 | 26 | 30 | 34 | 37 | 40 | 43 | 46 | 57 | 60 | -------------------------------------------------------------------------------- /SampleApp/Themes/FluentChartControl.axaml: -------------------------------------------------------------------------------- 1 | 4 | 20 | 24 | 28 | 32 | 36 | 39 | 42 | 45 | 48 | 57 | 62 | -------------------------------------------------------------------------------- /SatialInterfaces/Controls/ChartPoint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using Avalonia; 4 | using Avalonia.Controls; 5 | using Avalonia.Controls.Metadata; 6 | using Avalonia.Controls.Shapes; 7 | using Avalonia.LogicalTree; 8 | 9 | namespace SatialInterfaces.Controls.Chart; 10 | 11 | /// This interface represents a chart point. 12 | public interface IChartPoint : ILogical 13 | { 14 | /// Index property. 15 | int Index { get; set; } 16 | /// The X position. 17 | object? X { get; set; } 18 | /// The Y position. 19 | object? Y { get; set; } 20 | /// 21 | /// Converts the X-value to a double. 22 | /// 23 | /// The double or NaN otherwise. 24 | double XToDouble(); 25 | /// 26 | /// Converts the Y-value to a double. 27 | /// 28 | /// The double or NaN otherwise. 29 | double YToDouble(); 30 | } 31 | 32 | /// This class represents a chart point. 33 | [PseudoClasses(":pressed", ":selected")] 34 | public class ChartPoint : Ellipse, IChartPoint 35 | { 36 | /// X position property. 37 | public static readonly DirectProperty XProperty = AvaloniaProperty.RegisterDirect(nameof(X), o => o.X, (o, v) => o.X = v); 38 | /// Y position property. 39 | public static readonly DirectProperty YProperty = AvaloniaProperty.RegisterDirect(nameof(Y), o => o.Y, (o, v) => o.Y = v); 40 | /// Is selected property. 41 | public bool IsSelected { get => isSelected; set { isSelected = value; PseudoClasses.Set(":selected", isSelected); } } 42 | /// 43 | public int Index { get; set; } 44 | /// 45 | public object? X 46 | { 47 | get => x; set 48 | { 49 | SetAndRaise(XProperty, ref x, value); 50 | cachedX = new double?(); 51 | } 52 | } 53 | /// 54 | public object? Y 55 | { 56 | get => y; set 57 | { 58 | SetAndRaise(YProperty, ref y, value); 59 | cachedY = new double?(); 60 | } 61 | } 62 | 63 | /// Is selected. 64 | bool isSelected; 65 | /// X position. 66 | object? x; 67 | /// Y position. 68 | object? y; 69 | 70 | /// 71 | public double XToDouble() 72 | { 73 | cachedX ??= ToDouble(x); 74 | return cachedX.Value; 75 | } 76 | 77 | /// 78 | public double YToDouble() 79 | { 80 | cachedY ??= ToDouble(y); 81 | return cachedY.Value; 82 | } 83 | 84 | /// 85 | /// Converts the given object to a double. 86 | /// 87 | /// Value to convert. 88 | /// The double or NaN otherwise. 89 | static double ToDouble(object? value) 90 | { 91 | switch (value) 92 | { 93 | case null: 94 | return double.NaN; 95 | // Most common types 96 | case double d: 97 | return d; 98 | case float f: 99 | return f; 100 | case int i: 101 | return i; 102 | case long ul: 103 | return ul; 104 | case DateTime dt: 105 | return dt.Ticks; 106 | case byte b: 107 | return b; 108 | case sbyte sb: 109 | return sb; 110 | case char c: 111 | return c; 112 | case short s: 113 | return s; 114 | case ushort us: 115 | return us; 116 | case uint ui: 117 | return ui; 118 | } 119 | 120 | // Use the more expensive Converters 121 | var targetType = typeof(double); 122 | var converter = TypeDescriptor.GetConverter(targetType); 123 | try 124 | { 125 | if (converter.CanConvertFrom(value.GetType())) 126 | return (double)converter.ConvertFrom(value); 127 | return double.NaN; 128 | } 129 | catch (ArgumentException) 130 | { 131 | return double.NaN; 132 | } 133 | catch (NotSupportedException) 134 | { 135 | return double.NaN; 136 | } 137 | } 138 | 139 | /// 140 | /// Cached x value. 141 | /// 142 | double? cachedX; 143 | /// 144 | /// Cached y value. 145 | /// 146 | double? cachedY; 147 | } -------------------------------------------------------------------------------- /SampleApp/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using Avalonia; 7 | using Avalonia.Collections; 8 | using Avalonia.Controls; 9 | using Avalonia.Data; 10 | using Avalonia.Data.Converters; 11 | using Avalonia.Interactivity; 12 | using Avalonia.Markup.Xaml; 13 | 14 | namespace SampleApp.Views; 15 | 16 | public class ChartPointViewModel 17 | { 18 | public DateTime X { get; set; } 19 | public double Y { get; set; } 20 | } 21 | 22 | public class XValueConverter : IValueConverter 23 | { 24 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 25 | { 26 | return BindingOperations.DoNothing; 27 | } 28 | 29 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 30 | { 31 | if (targetType != typeof(string)) 32 | return BindingOperations.DoNothing; 33 | 34 | return value is ChartPointViewModel point ? point.X.ToShortDateString() + " " + point.X.ToLongTimeString() : BindingOperations.DoNothing; 35 | } 36 | } 37 | 38 | public class YValueConverter : IValueConverter 39 | { 40 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 41 | { 42 | return BindingOperations.DoNothing; 43 | } 44 | 45 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 46 | { 47 | if (targetType != typeof(string)) 48 | return BindingOperations.DoNothing; 49 | 50 | return value is ChartPointViewModel point ? string.Format(CultureInfo.CurrentCulture, "{0:F2} kg", point.Y) : BindingOperations.DoNothing; 51 | } 52 | } 53 | public partial class MainWindow : Window 54 | { 55 | /// 56 | /// Identifies the styled property. 57 | /// 58 | public static readonly StyledProperty XAxisTitleProperty = AvaloniaProperty.Register(nameof(XAxisTitle), string.Empty); 59 | /// 60 | /// Identifies the styled property. 61 | /// 62 | public static readonly StyledProperty YAxisTitleProperty = AvaloniaProperty.Register(nameof(YAxisTitle), string.Empty); 63 | /// Items property 64 | public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); 65 | 66 | /// X-axis property. 67 | public string XAxisTitle { get => GetValue(XAxisTitleProperty); set => SetValue(XAxisTitleProperty, value); } 68 | /// Y-axis property. 69 | public string YAxisTitle { get => GetValue(YAxisTitleProperty); set => SetValue(YAxisTitleProperty, value); } 70 | /// Items property. 71 | public IEnumerable Items { get => items; set => SetAndRaise(ItemsProperty, ref items, value); } 72 | 73 | #pragma warning disable CS8618 74 | public MainWindow() 75 | #pragma warning restore CS8618 76 | { 77 | InitializeComponent(); 78 | #if DEBUG 79 | this.AttachDevTools(); 80 | #endif 81 | } 82 | void InitializeComponent() 83 | { 84 | AvaloniaXamlLoader.Load(this); 85 | 86 | RandomItems(); 87 | } 88 | 89 | void RandomItems() 90 | { 91 | var now = DateTime.Now; 92 | var list = new List(); 93 | var count = GetRandom(1, 256); 94 | var xDivider = GetRandom(1, 256); 95 | var yDivider = GetRandom(1, 256); 96 | var xOffset = GetRandom(0, 255); 97 | var yOffset = GetRandom(0, 255); 98 | for (var i = 0; i < count; i++) 99 | { 100 | var x = (GetRandom(0, 255) - xOffset) / (double)xDivider; 101 | var y = (GetRandom(0, 255) - yOffset) / (double)yDivider; 102 | list.Add(new ChartPointViewModel { X = now.AddMinutes(x), Y = y }); 103 | } 104 | list = list.OrderBy(x => x.X).ToList(); 105 | Items = list; 106 | } 107 | 108 | #pragma warning disable RCS1213 109 | void RandomButtonClick(object? sender, RoutedEventArgs e) => RandomItems(); 110 | #pragma warning restore RCS1213 111 | 112 | static int GetRandom(int minVal, int maxVal) => Random.Next(minVal, maxVal + 1); 113 | 114 | static readonly Random Random = new(); 115 | /// Items. 116 | IEnumerable items = new AvaloniaList(); 117 | } -------------------------------------------------------------------------------- /SatialInterfaces/Controls/ChartControl.axaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /SatialInterfaces/Controls/ChartControl.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using Avalonia; 7 | using Avalonia.Collections; 8 | using Avalonia.Controls; 9 | using Avalonia.Controls.Shapes; 10 | using Avalonia.Controls.Templates; 11 | using Avalonia.Data; 12 | using Avalonia.Data.Converters; 13 | using Avalonia.Input; 14 | using Avalonia.Interactivity; 15 | using Avalonia.LogicalTree; 16 | using Avalonia.Markup.Xaml; 17 | using Avalonia.Media; 18 | using Avalonia.Styling; 19 | using SatialInterfaces.Helpers; 20 | 21 | namespace SatialInterfaces.Controls.Chart; 22 | 23 | internal class MarginConverter : IValueConverter 24 | { 25 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => 26 | value is double d ? new Thickness(0.0, 0.0, 0.0, d) : BindingOperations.DoNothing; 27 | 28 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => 29 | BindingOperations.DoNothing; 30 | } 31 | 32 | internal class VerticalControlMarginConverter : IMultiValueConverter 33 | { 34 | public object Convert(IList values, Type targetType, object? parameter, CultureInfo culture) 35 | { 36 | if (values.Count < 6 || 37 | values[0] is not double fontSize || 38 | values[1] is not string text || 39 | values[2] is not TextBlock textBlock || 40 | values[3] is not double parentWidth || 41 | values[4] is not double parentHeight || 42 | values[5] is not Thickness parentMargin) 43 | { 44 | return BindingOperations.DoNothing; 45 | } 46 | // textBlock.Measure(Size.Infinity); 47 | var d = 0.0 - fontSize / 2.0 + parentWidth / 2.0; 48 | // d = d + ((parentHeight - parentMargin.Bottom - textBlock.DesiredSize.Width) / 2); 49 | return new Thickness(0.0, 0.0, 0.0, d); 50 | } 51 | } 52 | 53 | /// This class represents a chart control (line with markers). 54 | public partial class ChartControl : ContentControl 55 | { 56 | /// Items property 57 | public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); 58 | /// Item template property. 59 | public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); 60 | /// Grid stroke property. 61 | public static readonly StyledProperty GridStrokeProperty = AvaloniaProperty.Register(nameof(GridStroke)); 62 | /// Grid stroke thickness property. 63 | public static readonly StyledProperty GridStrokeThicknessProperty = AvaloniaProperty.Register(nameof(GridStrokeThickness)); 64 | /// Line stroke property. 65 | public static readonly StyledProperty LineStrokeProperty = AvaloniaProperty.Register(nameof(LineStroke)); 66 | /// Line stroke thickness property. 67 | public static readonly StyledProperty LineStrokeThicknessProperty = AvaloniaProperty.Register(nameof(LineStrokeThickness)); 68 | /// The selected index property 69 | public static readonly StyledProperty SelectedIndexProperty = AvaloniaProperty.Register(nameof(SelectedIndex), -1); 70 | /// The selected item property 71 | public static readonly StyledProperty SelectedItemProperty = AvaloniaProperty.Register(nameof(SelectedItem)); 72 | /// X-axis title property. 73 | public static readonly StyledProperty XAxisTitleProperty = AvaloniaProperty.Register(nameof(XAxisTitle), string.Empty); 74 | /// Y-values maximum property. 75 | public static readonly StyledProperty XMaximumProperty = AvaloniaProperty.Register(nameof(XMaximum)); 76 | /// Y-values minimum property. 77 | public static readonly StyledProperty XMinimumProperty = AvaloniaProperty.Register(nameof(XMinimum)); 78 | /// X-value converter property. 79 | public static readonly DirectProperty XValueConverterProperty = AvaloniaProperty.RegisterDirect(nameof(XValueConverter), c => c.XValueConverter, (o, v) => o.XValueConverter = v); 80 | /// Y-axis title property. 81 | public static readonly StyledProperty YAxisTitleProperty = AvaloniaProperty.Register(nameof(YAxisTitle), string.Empty); 82 | /// Y-values maximum property. 83 | public static readonly StyledProperty YMaximumProperty = AvaloniaProperty.Register(nameof(YMaximum)); 84 | /// Y-values minimum property. 85 | public static readonly StyledProperty YMinimumProperty = AvaloniaProperty.Register(nameof(YMinimum)); 86 | /// Y-value converter property. 87 | public static readonly DirectProperty YValueConverterProperty = AvaloniaProperty.RegisterDirect(nameof(YValueConverter), c => c.YValueConverter, (o, v) => o.YValueConverter = v); 88 | /// The selection changed event 89 | public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble); 90 | 91 | /// Items property. 92 | public IEnumerable Items { get => items; set => SetAndRaise(ItemsProperty, ref items, value); } 93 | /// Item template property. 94 | public IDataTemplate? ItemTemplate { get => GetValue(ItemTemplateProperty); set => SetValue(ItemTemplateProperty, value); } 95 | /// Grid stroke property. 96 | public IBrush? GridStroke { get => GetValue(GridStrokeProperty); set => SetValue(GridStrokeProperty, value); } 97 | /// Grid stroke thickness. 98 | public double GridStrokeThickness { get => GetValue(GridStrokeThicknessProperty); set => SetValue(GridStrokeThicknessProperty, value); } 99 | /// Line stroke property. 100 | public IBrush? LineStroke { get => GetValue(LineStrokeProperty); set => SetValue(LineStrokeProperty, value); } 101 | /// Line stroke thickness. 102 | public double LineStrokeThickness { get => GetValue(LineStrokeThicknessProperty); set => SetValue(LineStrokeThicknessProperty, value); } 103 | /// Selected index 104 | public int SelectedIndex { get => GetValue(SelectedIndexProperty); set => SetValue(SelectedIndexProperty, value); } 105 | /// Selected item 106 | public object? SelectedItem { get => GetValue(SelectedItemProperty); set => SetValue(SelectedItemProperty, value); } 107 | /// X-axis property. 108 | public string XAxisTitle { get => GetValue(XAxisTitleProperty); set => SetValue(XAxisTitleProperty, value); } 109 | /// X-values maximum property. 110 | public object? XMaximum { get => GetValue(XMaximumProperty); private set => SetValue(XMaximumProperty, value); } 111 | /// X-values maximum property. 112 | public object? XMinimum { get => GetValue(XMinimumProperty); private set => SetValue(XMinimumProperty, value); } 113 | /// X-value converter property. 114 | public IValueConverter? XValueConverter { get => xValueConverter; set => SetAndRaise(XValueConverterProperty, ref xValueConverter, value); } 115 | /// Y-axis property. 116 | public string YAxisTitle { get => GetValue(YAxisTitleProperty); set => SetValue(YAxisTitleProperty, value); } 117 | /// Y-values maximum property. 118 | public object? YMaximum { get => GetValue(YMaximumProperty); private set => SetValue(YMaximumProperty, value); } 119 | /// Y-values maximum property. 120 | public object? YMinimum { get => GetValue(YMinimumProperty); private set => SetValue(YMinimumProperty, value); } 121 | /// Y-value converter property. 122 | public IValueConverter? YValueConverter { get => yValueConverter; set => SetAndRaise(YValueConverterProperty, ref yValueConverter, value); } 123 | /// Occurs when selection changed 124 | public event EventHandler SelectionChanged { add => AddHandler(SelectionChangedEvent, value); remove => RemoveHandler(SelectionChangedEvent, value); } 125 | 126 | /// Initializes static members of the class 127 | static ChartControl() 128 | { 129 | FocusableProperty.OverrideDefaultValue(true); 130 | 131 | ItemsProperty.Changed.AddClassHandler((x, e) => x.ItemsChanged(e)); 132 | SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.SelectedIndexChanged(e)); 133 | SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e)); 134 | } 135 | 136 | /// 137 | /// Initializes a new instance of the class. 138 | /// 139 | public ChartControl() 140 | { 141 | AvaloniaXamlLoader.Load(this); 142 | canvas = this.FindControl("Canvas"); 143 | polyline = this.FindControl("Polyline"); 144 | xMinimumTextBlock = this.FindControl("XMinimumTextBlock"); 145 | xMaximumTextBlock = this.FindControl("XMaximumTextBlock"); 146 | yMinimumTextBlock = this.FindControl("YMinimumTextBlock"); 147 | yMaximumTextBlock = this.FindControl("YMaximumTextBlock"); 148 | 149 | canvas?.GetObservable(BoundsProperty).Subscribe(OnCanvasBoundsChanged); 150 | } 151 | 152 | /// 153 | protected override void OnPointerPressed(PointerPressedEventArgs e) 154 | { 155 | leftButtonDown = e.GetCurrentPoint(this).Properties.IsLeftButtonPressed; 156 | base.OnPointerPressed(e); 157 | } 158 | 159 | /// 160 | protected override void OnPointerReleased(PointerReleasedEventArgs e) 161 | { 162 | if (!Items.Any() || !leftButtonDown || e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) 163 | { 164 | leftButtonDown = false; 165 | base.OnPointerReleased(e); 166 | return; 167 | } 168 | 169 | var control = e.Pointer.Captured as ILogical; 170 | var chartPoint = control as ChartPoint; 171 | var index = chartPoint?.Index ?? -1; 172 | leftButtonDown = false; 173 | base.OnPointerReleased(e); 174 | var previousIndex = SelectedIndex; 175 | SelectedIndex = index; 176 | // Force re-trigger 177 | if (index == previousIndex) 178 | RaiseSelectionChanged(index); 179 | } 180 | 181 | /// 182 | protected override void OnKeyDown(KeyEventArgs e) 183 | { 184 | switch (e.Key) 185 | { 186 | case Key.Up or Key.Left: 187 | SelectNext(-1); 188 | e.Handled = true; 189 | break; 190 | case Key.Down or Key.Right: 191 | SelectNext(1); 192 | e.Handled = true; 193 | break; 194 | } 195 | 196 | base.OnKeyDown(e); 197 | } 198 | 199 | /// 200 | protected override Type StyleKeyOverride => typeof(ChartControl); 201 | 202 | /// 203 | /// Items changed event. 204 | /// 205 | /// Argument for the event. 206 | void ItemsChanged(AvaloniaPropertyChangedEventArgs e) => UpdateItems(); 207 | 208 | /// 209 | /// Selected index changed event. 210 | /// 211 | /// Argument for the event. 212 | void SelectedIndexChanged(AvaloniaPropertyChangedEventArgs e) 213 | { 214 | if (skipSelectedIndexChanged || e.NewValue is not int value) return; 215 | SetSelection(value); 216 | RaiseSelectionChanged(value); 217 | } 218 | 219 | /// 220 | /// Selected item changed event. 221 | /// 222 | /// Argument for the event. 223 | void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) 224 | { 225 | if (skipSelectedItemChanged || e.NewValue is not { } value) return; 226 | var index = GetItemsAsList().IndexOf(value); 227 | SetSelection(index); 228 | RaiseSelectionChanged(value); 229 | } 230 | 231 | /// 232 | /// Canvas bounds changed: adjust scrollable grid as well. 233 | /// /// 234 | /// Rectangle of the canvas. 235 | void OnCanvasBoundsChanged(Rect rect) => UpdateItems(); 236 | 237 | /// 238 | /// Raises the selection changed event. 239 | /// 240 | /// Index selected. 241 | void RaiseSelectionChanged(int index) 242 | { 243 | object? item = null; 244 | var list = GetItemsAsList(); 245 | if (index >= 0 && index < list.Count) 246 | item = list[index]; 247 | var eventArgs = new ChartSelectionChangedEventArgs(SelectionChangedEvent) 248 | { SelectedIndex = index, SelectedItem = item }; 249 | RaiseEvent(eventArgs); 250 | skipSelectedItemChanged = true; 251 | SelectedItem = item; 252 | skipSelectedItemChanged = false; 253 | } 254 | 255 | /// 256 | /// Sets the selection bit for the selected chart point. 257 | /// 258 | /// Index of the selected item. 259 | void SetSelection(int selectedIndex) 260 | { 261 | if (canvas == null) return; 262 | var chartPoints = canvas.GetLogicalDescendants().OfType().ToList(); 263 | var chartPointIndex = chartPoints.FindIndex(x => x.Index == selectedIndex); 264 | 265 | for (var i = 0; i < chartPoints.Count; i++) 266 | chartPoints[i].IsSelected = i == chartPointIndex; 267 | } 268 | 269 | /// 270 | /// Raises the selection changed event. 271 | /// 272 | /// Item selected. 273 | void RaiseSelectionChanged(object item) 274 | { 275 | var index = GetItemsAsList().IndexOf(item); 276 | 277 | var eventArgs = new ChartSelectionChangedEventArgs(SelectionChangedEvent) 278 | { SelectedIndex = index, SelectedItem = item }; 279 | RaiseEvent(eventArgs); 280 | skipSelectedIndexChanged = true; 281 | SelectedIndex = index; 282 | skipSelectedIndexChanged = false; 283 | } 284 | 285 | /// 286 | /// Select next chart point. 287 | /// 288 | /// Step to take. 289 | void SelectNext(int step) 290 | { 291 | if (canvas == null) return; 292 | var chartPoints = canvas.GetLogicalDescendants().OfType().ToList(); 293 | if (chartPoints.Count == 0) return; 294 | var chartPointIndex = chartPoints.FindIndex(x => x.Index == SelectedIndex); 295 | chartPointIndex += step; 296 | if (chartPointIndex < 0) 297 | chartPointIndex = chartPoints.Count - 1; 298 | else if (chartPointIndex >= chartPoints.Count) 299 | chartPointIndex = 0; 300 | SelectedItem = chartPoints[chartPointIndex].DataContext; 301 | } 302 | 303 | /// 304 | /// Updates the items in the view. 305 | /// 306 | void UpdateItems() 307 | { 308 | if (canvas == null || polyline == null) return; 309 | ClearCanvasItems(); 310 | if (Items == null) return; 311 | var indexedItems = Items.ToList(); 312 | var chartPoints = Convert(indexedItems); 313 | if (chartPoints.Count == 0) 314 | { 315 | polyline.Points = new List(); 316 | XMinimum = null; 317 | XMaximum = null; 318 | YMinimum = null; 319 | YMaximum = null; 320 | UpdateMinimaMaximaTextBlocks(); 321 | return; 322 | } 323 | 324 | var points = new List(); 325 | 326 | var stats = GetStatistics(chartPoints); 327 | var xMinimum = chartPoints[stats.XMinimumIndex].XToDouble(); 328 | var xMaximum = chartPoints[stats.XMaximumIndex].XToDouble(); 329 | var yMinimum = chartPoints[stats.YMinimumIndex].YToDouble(); 330 | var yMaximum = chartPoints[stats.YMaximumIndex].YToDouble(); 331 | var xScale = canvas.Bounds.Width / (xMaximum - xMinimum); 332 | var yScale = canvas.Bounds.Height / (yMaximum - yMinimum); 333 | foreach (var item in chartPoints) 334 | { 335 | var x = (item.XToDouble() - xMinimum) * xScale; 336 | var y = canvas.Bounds.Height - (item.YToDouble() - yMinimum) * yScale; 337 | 338 | points.Add(new Point(x, y)); 339 | canvas.Children.Add(item); 340 | Canvas.SetLeft(item, x - (item.Width / 2.0)); 341 | Canvas.SetTop(item, y - (item.Height / 2.0)); 342 | } 343 | 344 | var xIsDateTime = chartPoints[stats.XMinimumIndex].X is DateTime; 345 | var yIsDateTime = chartPoints[stats.YMinimumIndex].Y is DateTime; 346 | DrawXGridLines(xMinimum, xMaximum, xIsDateTime); 347 | DrawYGridLines(yMinimum, yMaximum, yIsDateTime); 348 | polyline.Points = points; 349 | YMinimum = indexedItems[stats.YMinimumIndex]; 350 | YMaximum = indexedItems[stats.YMaximumIndex]; 351 | XMinimum = indexedItems[stats.XMinimumIndex]; 352 | XMaximum = indexedItems[stats.XMaximumIndex]; 353 | UpdateMinimaMaximaTextBlocks(); 354 | } 355 | 356 | /// 357 | /// Updates the minimum and maximum text blocks. 358 | /// 359 | void UpdateMinimaMaximaTextBlocks() 360 | { 361 | if (xMinimumTextBlock != null) xMinimumTextBlock.Text = ConvertXValueToText(XMinimum); 362 | if (xMaximumTextBlock != null) xMaximumTextBlock.Text = ConvertXValueToText(XMaximum); 363 | if (yMinimumTextBlock != null) yMinimumTextBlock.Text = ConvertYValueToText(YMinimum); 364 | if (yMaximumTextBlock != null) yMaximumTextBlock.Text = ConvertYValueToText(YMaximum); 365 | } 366 | 367 | /// 368 | /// Gets the items as a list. 369 | /// 370 | /// The list. 371 | IList GetItemsAsList() => Items as IList ?? Items.ToList(); 372 | 373 | static (int XMinimumIndex, int XMaximumIndex, int YMinimumIndex, int YMaximumIndex) GetStatistics(IList items) 374 | { 375 | var xMinimumIndex = -1; 376 | var xMaximumIndex = -1; 377 | var yMinimumIndex = -1; 378 | var yMaximumIndex = -1; 379 | var xMinimum = double.NaN; 380 | var xMaximum = double.NaN; 381 | var yMinimum = double.NaN; 382 | var yMaximum = double.NaN; 383 | 384 | for (var i = 0; i < items.Count; i++) 385 | { 386 | if (i == 0) 387 | { 388 | xMinimumIndex = i; 389 | xMaximumIndex = i; 390 | yMinimumIndex = i; 391 | yMaximumIndex = i; 392 | xMinimum = items[i].XToDouble(); 393 | xMaximum = items[i].XToDouble(); 394 | yMinimum = items[i].YToDouble(); 395 | yMaximum = items[i].YToDouble(); 396 | continue; 397 | } 398 | if (items[i].XToDouble() < xMinimum) 399 | { 400 | xMinimumIndex = i; 401 | xMinimum = items[i].XToDouble(); 402 | } 403 | if (items[i].XToDouble() > xMaximum) 404 | { 405 | xMaximumIndex = i; 406 | xMaximum = items[i].XToDouble(); 407 | } 408 | if (items[i].YToDouble() < yMinimum) 409 | { 410 | yMinimumIndex = i; 411 | yMinimum = items[i].YToDouble(); 412 | } 413 | if (items[i].YToDouble() > yMaximum) 414 | { 415 | yMaximumIndex = i; 416 | yMaximum = items[i].YToDouble(); 417 | } 418 | } 419 | return (xMinimumIndex, xMaximumIndex, yMinimumIndex, yMaximumIndex); 420 | } 421 | 422 | /// 423 | /// Clears the canvas items except the polyline. 424 | /// 425 | void ClearCanvasItems() 426 | { 427 | if (canvas == null || polyline == null) return; 428 | var i = 0; 429 | while (i < canvas.Children.Count) 430 | { 431 | if (!ReferenceEquals(canvas.Children[i], polyline)) 432 | canvas.Children.RemoveAt(i); 433 | else 434 | i++; 435 | } 436 | } 437 | 438 | /// 439 | /// Draws the grid lines for the X-axis. 440 | /// 441 | /// Minimum of the X-values. 442 | /// Maximum of the X-values. 443 | /// Flag if the values are date/time objects. 444 | void DrawXGridLines(double minimumX, double maximumX, bool isDateTime) 445 | { 446 | if (canvas == null) return; 447 | GetGridValues(minimumX, maximumX, isDateTime, out var range, out var stepSize, out var beginValue); 448 | var list = new List(); 449 | for (var d = beginValue; d < maximumX; d += stepSize) 450 | { 451 | var x = (d - minimumX) / range * canvas.Bounds.Width; 452 | if (x < 0.0 || x > canvas.Bounds.Width) 453 | continue; 454 | var line = new Line { StartPoint = new Point(x, 0.0), EndPoint = new Point(x, canvas.Bounds.Height) }; 455 | BindGridLine(line); 456 | list.Add(line); 457 | } 458 | canvas.Children.InsertRange(0, list); 459 | } 460 | 461 | /// 462 | /// Draws the grid lines for the Y-axis. 463 | /// 464 | /// Minimum of the Y-values. 465 | /// Maximum of the XY-values. 466 | /// Flag if the values are date/time objects. 467 | void DrawYGridLines(double minimumY, double maximumY, bool isDateTime) 468 | { 469 | if (canvas == null) return; 470 | GetGridValues(minimumY, maximumY, isDateTime, out var range, out var stepSize, out var beginValue); 471 | var list = new List(); 472 | for (var d = beginValue; d < maximumY; d += stepSize) 473 | { 474 | var y = (1.0 - ((d - minimumY) / range)) * canvas.Bounds.Height; 475 | if (y < 0.0 || y > canvas.Bounds.Height) 476 | continue; 477 | var line = new Line { StartPoint = new Point(0.0, y), EndPoint = new Point(canvas.Bounds.Width, y) }; 478 | BindGridLine(line); 479 | list.Add(line); 480 | } 481 | canvas.Children.InsertRange(0, list); 482 | } 483 | 484 | /// 485 | /// Gets the grid values: range, step size and begin value 486 | /// 487 | /// Minimum of values. 488 | /// Maximum of values. 489 | /// Flag if the values are date/time objects. 490 | /// [out] The range. 491 | /// [out] The step size. 492 | /// [out] The begin value. 493 | static void GetGridValues(double minimum, double maximum, bool isDateTime, out double range, out double stepSize, out double beginValue) 494 | { 495 | range = maximum - minimum; 496 | if (isDateTime) 497 | { 498 | stepSize = (range / 1e7) switch 499 | { 500 | < 60.0 => 1e7 * 1.0,// Seconds 501 | < 3600.0 => 1e7 * 60.0,// Minutes 502 | < 86400.0 => 1e7 * 3600.0,// Hours 503 | < 365.2425 * 86400.0 => 1e7 * 86400.0,// Days 504 | _ => 1e7 * (365.2425 * 86400.0)// Years 505 | }; 506 | } 507 | else 508 | { 509 | stepSize = GridHelper.GetGridSize(range, 8); 510 | } 511 | beginValue = GridHelper.RoundUp(minimum, stepSize); 512 | } 513 | 514 | /// 515 | /// Bind the given line to the grid properties of this instance. 516 | /// 517 | /// Line to bind. 518 | void BindGridLine(AvaloniaObject line) 519 | { 520 | var binding = new Binding("GridStroke") 521 | { 522 | RelativeSource = new RelativeSource(RelativeSourceMode.Self), 523 | Source = this 524 | }; 525 | line.Bind(Shape.StrokeProperty, binding); 526 | binding = new Binding("GridStrokeThickness") 527 | { 528 | RelativeSource = new RelativeSource(RelativeSourceMode.Self), 529 | Source = this 530 | }; 531 | line.Bind(Shape.StrokeThicknessProperty, binding); 532 | } 533 | 534 | /// 535 | /// Converts the given items to a handleable format. 536 | /// 537 | /// Items to process. 538 | /// Internal handleable format. 539 | IList Convert(IEnumerable enumerable) 540 | { 541 | var result = new List(); 542 | if (!CanBuildItems()) return result; 543 | { 544 | var i = 0; 545 | foreach (var e in enumerable) 546 | { 547 | var p = Convert(e, i); 548 | result.Add(p); 549 | i++; 550 | } 551 | } 552 | 553 | return result; 554 | } 555 | 556 | /// 557 | /// Converts an item from the source (items) to its equivalent control. 558 | /// 559 | /// Object from items. 560 | /// Index to assign. 561 | /// The control. 562 | ChartPoint Convert(object o, int index) 563 | { 564 | var p = BuildItem(o); 565 | p.DataContext = o; 566 | p.Index = index; 567 | return p; 568 | } 569 | 570 | /// 571 | /// Checks if an item can be built. 572 | /// 573 | /// True if it can and false otherwise. 574 | bool CanBuildItems() => ItemTemplate != null; 575 | 576 | /// 577 | /// Builds the item (control). 578 | /// 579 | /// Parameter to supply to the item template builder. 580 | /// The item. 581 | ChartPoint BuildItem(object param) 582 | { 583 | var result = ItemTemplate?.Build(param) as ChartPoint; 584 | return result ?? new ChartPoint 585 | { 586 | [!ChartPoint.XProperty] = new Binding("X"), 587 | [!ChartPoint.YProperty] = new Binding("Y") 588 | }; 589 | } 590 | 591 | /// 592 | /// Converts the given x-value to formatted text. 593 | /// 594 | /// X-value to convert. 595 | /// The string. 596 | string ConvertXValueToText(object? xValue) => ValueConverterHelper.ConvertValueToText(xValueConverter, xValue); 597 | 598 | /// 599 | /// Converts the given y-value to formatted text. 600 | /// 601 | /// Y-value to convert. 602 | /// The string. 603 | string ConvertYValueToText(object? yValue) => ValueConverterHelper.ConvertValueToText(yValueConverter, yValue); 604 | 605 | /// Items. 606 | IEnumerable items = new AvaloniaList(); 607 | /// State of the left mouse button 608 | bool leftButtonDown; 609 | /// Skip handling the selected index changed event flag. 610 | bool skipSelectedIndexChanged; 611 | /// Skip handling the selected item changed event flag. 612 | bool skipSelectedItemChanged; 613 | /// X-value converter. 614 | IValueConverter? xValueConverter = new DefaultXValueConverter(); 615 | /// Y-value converter. 616 | IValueConverter? yValueConverter = new DefaultYValueConverter(); 617 | /// Canvas. 618 | readonly Canvas? canvas; 619 | /// Polygon. 620 | readonly Polyline? polyline; 621 | /// X-values minimum text block. 622 | readonly TextBlock? xMinimumTextBlock; 623 | /// X-values maximum text block. 624 | readonly TextBlock? xMaximumTextBlock; 625 | /// Y-values minimum text block. 626 | readonly TextBlock? yMinimumTextBlock; 627 | /// Y-values maximum text block. 628 | readonly TextBlock? yMaximumTextBlock; 629 | } --------------------------------------------------------------------------------