Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,55 @@
# Building-Variance-Indicators-in-.NET-MAUI-Toolkit-Charts-to-Visualize-Natural-Gas-Price-Volatility
Discover techniques to visualize natural gas price volatility with dynamic variance indicators using .NET MAUI Toolkit Charts from Syncfusion. Learn how to implement custom arrow indicators with percentage labels by extending range column series to deliver comprehensive energy market trend visualization for better insights
# Visualizing Natural Gas Price Volatility with Custom Variance Indicators in .NET MAUI

Discover how to build sophisticated price volatility visualizations using variance indicators with [Syncfusion's .NET MAUI Toolkit Charts](https://www.syncfusion.com/maui-controls/maui-cartesian-charts).

## Overview

Energy market analysis demands clear visualization of both absolute price levels and price volatility. This project demonstrates how to create custom variance indicators that show percentage changes between time periods with directional arrows and color-coding, enhancing standard column charts for more insightful analysis.

The implementation leverages custom renderers in .NET MAUI Toolkit to create specialized variance indicators that visually represent price movement direction and magnitude. This approach is valuable for energy traders, market analysts, and financial professionals tracking commodity volatility.

## Key Features

### Advanced Variance Visualization

- Display both price levels and percentage changes in a single view
- Highlight price increases and decreases with color-coded indicators
- Show precise percentage changes between consecutive time periods
- Indicate direction with custom arrow elements

### Custom Chart Extensions

- Extend [RangeColumnSeries](https://help.syncfusion.com/cr/maui-toolkit/Syncfusion.Maui.Toolkit.Charts.RangeColumnSeries.html) for specialized variance indicators
- Implement custom `RangeColumnSegmentExt` for dynamic rendering
- Calculate volatility metrics on the fly
- Position visual elements based on data characteristics

### Interactive Data Analysis

- Access detailed data through custom tooltips
- Compare price levels across multiple time periods
- Identify volatility patterns with intuitive visual cues
- Assess market stability through visual trend analysis

## Technologies Used

- **.NET MAUI:** Cross-platform UI framework
- **Syncfusion .NET MAUI Toolkit:** Advanced charting capabilities
- **Custom Renderers:** Enhanced visualization components
- **C# & XAML:** Implementation languages

## Syncfusion Controls Used

This project showcases the powerful customization of two key Syncfusion controls:

- **[SfCartesianChart](https://help.syncfusion.com/cr/maui-toolkit/Syncfusion.Maui.Toolkit.Charts.SfCartesianChart.html):** Foundation for price visualization

![VarianceIndicators](https://github.com/user-attachments/assets/4930c677-6b33-4830-bc96-555def1157db)

## Troubleshooting

### Path Too Long Exception

If you are facing a "Path too long" exception when building this example project, close Visual Studio and rename the repository to a shorter name before building the project.

For a step-by-step procedure, refer to the blog:
14 changes: 14 additions & 0 deletions VarianceIndicators/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:VarianceIndicators"
x:Class="VarianceIndicators.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
15 changes: 15 additions & 0 deletions VarianceIndicators/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace VarianceIndicators
{
public partial class App : Application
{
public App()
{
InitializeComponent();
}

protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
}
}
13 changes: 13 additions & 0 deletions VarianceIndicators/AppShell.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="VarianceIndicators.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:VarianceIndicators"
Title="VarianceIndicators">

<ShellContent
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />

</Shell>
10 changes: 10 additions & 0 deletions VarianceIndicators/AppShell.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace VarianceIndicators
{
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
}
112 changes: 112 additions & 0 deletions VarianceIndicators/CustomCharts/CustomCharts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using Syncfusion.Maui.Toolkit.Charts;
using Syncfusion.Maui.Toolkit.Graphics.Internals;
using System.Collections.ObjectModel;
using Font = Microsoft.Maui.Graphics.Font;

namespace VarianceIndicators
{
public partial class RangeColumnSeriesExt : RangeColumnSeries
{
protected override ChartSegment CreateSegment()
{
return new RangeColumnSegmentExt();
}
}

public partial class RangeColumnSegmentExt : RangeColumnSegment
{
float offsetPercentage = 0.2f;
float topArrowTextGap = 1.0f;
float bottomArrowTextGap = 1.0f;

protected override void Draw(ICanvas canvas)
{
// Call base layout first
base.OnLayout();

// Skip drawing if segment isn't properly laid out or data is missing
if (float.IsNaN(Left) || float.IsNaN(Top) || float.IsNaN(Right) || float.IsNaN(Bottom) ||
Series is not RangeColumnSeriesExt || Item is not ChartDataModel dataPoint)
return;

// Skip last segment or if there's no change
var itemsSource = Series.ItemsSource as ObservableCollection<ChartDataModel>;

if (Index == itemsSource?.Count - 1 || dataPoint.Low == dataPoint.High)
return;

// Calculate change direction and percentage
bool isIncreasing = dataPoint.Low < dataPoint.High;
double changePercent = Math.Abs((dataPoint.High - dataPoint.Low) / dataPoint.Low) * 100;

// Set color based on direction
Color arrowColor = isIncreasing ? Colors.Green : Colors.Red;

// Ensure we have a valid drawing canvas
if (canvas == null) return;

// Save canvas state before drawing
canvas.SaveState();

float arrowSize = 6;

#if IOS
offsetPercentage = 0.1f;
topArrowTextGap = 2.0f;
#elif MACCATALYST
topArrowTextGap = 2.0f;
#elif WINDOWS
topArrowTextGap = 3.0f;
#elif ANDROID
bottomArrowTextGap = 1.5f;
#endif

float columnWidth = Right - Left;
float offsetX = columnWidth * offsetPercentage;
float centerX = (Left + Right) / 2 + offsetX;

// Draw line first
canvas.StrokeColor = Colors.Black;
canvas.StrokeSize = 1f;
canvas.DrawLine(centerX, Top, centerX, Bottom);

// Draw arrow
var arrowPath = new PathF();

// Draw arrow based on direction
if (isIncreasing)
{
arrowPath.MoveTo(centerX, Top);
arrowPath.LineTo(centerX - arrowSize, Top + arrowSize);
arrowPath.LineTo(centerX + arrowSize, Top + arrowSize);
}
else
{
arrowPath.MoveTo(centerX, Bottom);
arrowPath.LineTo(centerX - arrowSize, Bottom - arrowSize);
arrowPath.LineTo(centerX + arrowSize, Bottom - arrowSize);
}

arrowPath.Close();
canvas.FillColor = Colors.Black;
canvas.FillPath(arrowPath);

// Draw percentage text with clear formatting

string percentText = isIncreasing ?
$"{changePercent:0}%" :
$"- {changePercent:0}%";

var labelStyle = new ChartAxisLabelStyle() { FontSize = 12, TextColor = arrowColor };
var font = Font.Default;

// Measure text size using canvas directly

SizeF textSize = canvas.GetStringSize(percentText, font, (float)labelStyle.FontSize);

float textY = (float)(isIncreasing ? Top - (textSize.Height * topArrowTextGap) : Bottom + (textSize.Height * bottomArrowTextGap));
canvas.DrawText(percentText, centerX - (textSize.Width / 2), textY, labelStyle);
canvas.RestoreState();
}
}
}
120 changes: 120 additions & 0 deletions VarianceIndicators/MainPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="VarianceIndicators.MainPage"
xmlns:syncfusion="clr-namespace:Syncfusion.Maui.Toolkit.Charts;assembly=Syncfusion.Maui.Toolkit"
xmlns:local="clr-namespace:VarianceIndicators">

<ContentPage.BindingContext>
<local:ChartViewModel />
</ContentPage.BindingContext>

<ContentPage.Resources>
<x:String x:Key="PathData" >M24,17.000001L24,26.000002 27,26.000002 27,17.000001z M22,15.000001L29,15.000001 29,28.000002 22,28.000002z M6,14.000001L6,26.000002 9,26.000002 9,14.000001z M4,12.000001L11,12.000001 11,28.000002 4,28.000002z M15,4.0000005L15,26 18,26 18,4.0000005z M13,2.0000005L20,2.0000005 20,28 13,28z M0,0L2,0 2,30 32,30 32,32 0,32z</x:String>
</ContentPage.Resources>


<Border Margin="{OnPlatform Android=10,Default=25,iOS=10}"
StrokeThickness="1" Stroke="Black"
StrokeShape="RoundRectangle 15">

<syncfusion:SfCartesianChart HorizontalOptions="Fill"
VerticalOptions="Fill"
Margin="{OnPlatform Android=5,Default=15,iOS=5}">
<syncfusion:SfCartesianChart.Resources>
<DataTemplate x:Key="tooltipTemplate" >
<Border BackgroundColor="#7bb4eb"
Padding="8, 3, 8, 3"
Stroke="Transparent">
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Item.Year, StringFormat='{}{0:yyyy}'}"
TextColor="Black"
FontSize="14"
HorizontalOptions="Center"
VerticalOptions="Center"/>
<Label Text=" : "
TextColor="Black"
FontAttributes="Bold"
FontSize="14"
HorizontalOptions="Center"
VerticalOptions="Center"/>
<BoxView WidthRequest="5"
BackgroundColor="Transparent"/>
<Label Text="{Binding Item.YValue}"
TextColor="Black"
FontSize="14"
HorizontalOptions="Center"
VerticalOptions="Center"/>
</StackLayout>
</Border>
</DataTemplate>
</syncfusion:SfCartesianChart.Resources>
<syncfusion:SfCartesianChart.Title>
<Grid Padding="35,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Path Grid.Column="0"
Data="{StaticResource PathData}"
Fill="Black"
Stroke="Gray"
StrokeThickness="1"
HeightRequest="40"
WidthRequest="40"
VerticalOptions="Center"/>

<StackLayout Grid.Column="1" Orientation="Vertical" HorizontalOptions="Start">
<Label Text="Building Variance Indicators in .NET MAUI Toolkit Charts to Visualize Natural Gas Price Volatility"
FontAttributes="Bold"
FontSize="{OnPlatform Android=12,Default=16,iOS=12}" />
<Label Text="Natural Gas Price Volatility Analysis 2014-2024"
FontSize="{OnPlatform Android=10,Default=12,iOS=10}" />
</StackLayout>
</Grid>
</syncfusion:SfCartesianChart.Title>

<syncfusion:SfCartesianChart.TooltipBehavior>
<syncfusion:ChartTooltipBehavior Background="#7bb4eb" />
</syncfusion:SfCartesianChart.TooltipBehavior>

<syncfusion:SfCartesianChart.XAxes>
<syncfusion:DateTimeAxis Interval="2" IntervalType="Years" >
<syncfusion:DateTimeAxis.Title>
<syncfusion:ChartAxisTitle Text="Time Period (Years)" />
</syncfusion:DateTimeAxis.Title>
<syncfusion:DateTimeAxis.AxisLineStyle>
<syncfusion:ChartLineStyle Stroke="#888888"/>
</syncfusion:DateTimeAxis.AxisLineStyle>
</syncfusion:DateTimeAxis>
</syncfusion:SfCartesianChart.XAxes>

<syncfusion:SfCartesianChart.YAxes>
<syncfusion:NumericalAxis Maximum="5.5" Interval="1" >
<syncfusion:NumericalAxis.Title>
<syncfusion:ChartAxisTitle Text="Price (USD per Million BTU)" />
</syncfusion:NumericalAxis.Title>
<syncfusion:NumericalAxis.MajorGridLineStyle>
<syncfusion:ChartLineStyle StrokeWidth="{OnPlatform Android=0.0,iOS=0.0}" />
</syncfusion:NumericalAxis.MajorGridLineStyle>
</syncfusion:NumericalAxis>
</syncfusion:SfCartesianChart.YAxes>

<syncfusion:SfCartesianChart.Series>
<syncfusion:ColumnSeries ItemsSource="{Binding Data}"
Spacing="{OnPlatform Android=0.5,iOS=0.5}"
XBindingPath="Year"
YBindingPath="YValue"
EnableTooltip="True"
Fill="#7bb4eb"
ShowDataLabels="False"
TooltipTemplate="{StaticResource tooltipTemplate}">
</syncfusion:ColumnSeries>
<local:RangeColumnSeriesExt ItemsSource="{Binding Data}"
XBindingPath="Year"
High="High"
Low="Low" />
</syncfusion:SfCartesianChart.Series>
</syncfusion:SfCartesianChart>
</Border>
</ContentPage>
11 changes: 11 additions & 0 deletions VarianceIndicators/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

namespace VarianceIndicators
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
}
27 changes: 27 additions & 0 deletions VarianceIndicators/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Extensions.Logging;
using Syncfusion.Maui.Toolkit.Hosting;

namespace VarianceIndicators
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureSyncfusionToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();
}
}
}
Loading