How to draw 2D charts in .NET MAUI

In this tutorial, you will learn how to create a scrollable line-chart, scrollable bar-chart, and a pie chart using .NET MAUI Graphics. There is also a link to a Xamarin.Forms implementation using SkiaSharp for curious developers still using Xamarin, which at this time I assume is many of you. Creating these charts is a great excercise to learning Maui.Graphics and you can use these customizable components in your application.

.NET Maui is currently in preview and at the time of writing this article, it is only available in Visual Studio for Windows 2022 preview.

To get started you will need to download the latest edition of Visual Studio 2022 that contains the .NET Maui mobile app templates. Once you have the IDE setup, go ahead and create a new Maui mobile app.

Creating a Gradient Pie Chart

To keep things organized you should create a Charts folder to house the chart components we will create and within this folder create a PieChart folder.

The first thing we will do is create a IDrawable that will draw our pie chart. In Maui, a GraphicsView is the XAML component you can call to show a 2D component and the IDrawable is an attribute of the GraphicsView that is the actual 2D canvas. Add a new PieChartDrawable class to the PieChart folder and paste the following code.


internal class PieChartDrawable : View, IDrawable
{
    public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Points),
        typeof(Dictionary<string, float>),
        typeof(PieChartDrawable),
        new Dictionary<string, float>());

    public Dictionary<string, float> Points
    {
        get => (Dictionary<string, float>)GetValue(PointsProperty);
        set => SetValue(PointsProperty, value);
    }

    /// <summary>
    /// Converts degrees around a circle to a Point
    /// </summary>
    /// <param name="degrees">degree around a circle from zero to 360</param>
    /// <param name="radius">distance from the center of the circle</param>
    /// <param name="rect">rectange that contains the circle</param>
    /// <returns></returns>
    private PointF PointFromDegrees(float degrees, float radius, RectangleF rect, int padding = 0)
    {
        const int offset = 90;

        var x = (float)(rect.Center.X + (radius + padding) * Math.Cos((degrees - offset) * (Math.PI / 180)));
        var y = (float)(rect.Center.Y + (radius + padding) * Math.Sin((degrees - offset) * (Math.PI / 180)));

        return new PointF(x, y);
    }

    public void Draw(ICanvas canvas, RectangleF dirtyRect)
    {
        canvas.ResetState();

        var radius = dirtyRect.Width / 4;
        var purple = Color.FromRgba(178, 127, 255, 125);
        var translucent = Color.FromRgba(235, 222, 255, 0);
        canvas.FontColor = Color.FromArgb("#7F2CF6");
        var center = new PointF(dirtyRect.Center.X, dirtyRect.Center.Y);

        //Draw Circle 
        var radialGradientPaint = new RadialGradientPaint
        {
            EndColor = purple,
            StartColor = translucent
        };
            
        var radialRectangle = new RectangleF(dirtyRect.Center.X - radius, dirtyRect.Center.Y - radius,radius * 2, radius * 2);
        canvas.SetFillPaint(radialGradientPaint, radialRectangle);
        canvas.FillCircle(center, radius);

        var scale = 100f / Points.Select(x => x.Value).Sum();

        //Draw first initial line
        canvas.StrokeColor = Colors.White;
        canvas.DrawLine(
            new PointF(center.X, center.Y - radius),
            center);

        var lineDegrees = 0f;
        var textDegrees = 0f;
        var textRadiusPadding = Convert.ToInt32(dirtyRect.Width / 10);

        //Draw splits into pie using ?
        for (var i = 0; i < Points.Count; i++)
        {
            var point = Points.ElementAt(i);
            lineDegrees += 360 * (point.Value * scale / 100);
            textDegrees += (360 * (point.Value * scale / 100) / 2);

            var lineStartingPoint = PointFromDegrees(lineDegrees, radius, dirtyRect);
            var textPoint = PointFromDegrees(textDegrees, radius, dirtyRect, textRadiusPadding);
            var valuePoint = new PointF(textPoint.X, textPoint.Y + 15);

            canvas.DrawLine(
                    lineStartingPoint,
                    center);

            canvas.DrawString(point.Key,
                textPoint.X,
                textPoint.Y,
                HorizontalAlignment.Center);

            canvas.DrawString(point.Value.ToString(),
                valuePoint.X,
                valuePoint.Y,
               HorizontalAlignment.Center);

            textDegrees += (360 * (point.Value * scale / 100) / 2);
        }
    }
}
  • Points: The points property is a dictionary of string keys and int values. The values will be represented by points along the axes of the chart. It is in the form of a BindableProperty because we need to be able to pass the points from XAML to our PieChartDrawable class.
  • PointFromDegrees: This is a mathematical method that returns a coordinate around the circumference of a circle based on the degrees passed.
  • ResetState: Clears the canvas
  • radialRectangle: The rectangle that contains the circle used for the pie chart.
  • SetFillPaint: Assigns color to the designated pixels
  • FillCircle: Colors the circle
  • scale: The sum of all values divided by 100. This property is used to scale each slice of the pie to be a percentage.
  • textRadiusPadding: Adds distance out from the circumference of the circle which is used to position text.

Because we need to bind the Points property and IDrawable has no knowledge of our custom Points property we need to create a custom GraphicsView control that has this property. Add a new class to the PieChart folder with the name PieChartGraphicsView.

internal class PieChartGraphicsView : GraphicsView
{
    public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Points),
        typeof(Dictionary<string, float>),
        typeof(PieChartGraphicsView),
        new Dictionary<string, float>(),
        propertyChanged: async (bindable, oldValue, newValue) =>
        {
            var chartView = ((PieChartGraphicsView)bindable);

            chartView.PieChartDrawable.Points = (Dictionary<string, float>)newValue;
        });

    public Dictionary<string, float> Points
    {
        get => (Dictionary<string, float>)GetValue(PointsProperty);
        set => SetValue(PointsProperty, value);
    }

    public PieChartGraphicsView() => Drawable = PieChartDrawable;

    public PieChartDrawable PieChartDrawable = new PieChartDrawable();
}

Add a new Maui content page named PieChart to this folder. Although Xamarin content pages are still available you want to make sure you choose .NET MAUI ContentPage (XAML) (Preview) as this is the only content page that will work in a .NET Maui application. At this time there seems to be an issue where the IDrawable’s Draw method will not fire unless width and height are assigned. I’ve submitted a bug to resolve this and hopefully we will not need to assign these values in the future.

<StackLayout x:Class="MauiCharts.Charts.PieChart"
             xmlns:charts="clr-namespace:MauiCharts.Charts"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <charts:PieChartGraphicsView x:Name="Chart"
                                 WidthRequest="400"
                                 HeightRequest="700"
                                 Points="{Binding .}"/>
</StackLayout>

Since this content page is also part of our custom control, we need to add a Points BindableProperty in order for the control to work when called from client code.

public partial class PieChart : StackLayout
{
    public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Points),
            typeof(Dictionary<string, float>),
            typeof(PieChart),
            new Dictionary<string, float>(),
            propertyChanged: async (bindable, oldValue, newValue) =>
            {
                var chartView = ((PieChart)bindable);

                chartView.Chart.PieChartDrawable.Points = (Dictionary<string, float>)newValue;
            });

    public Dictionary<string, float> Points
    {
        get => (Dictionary<string, float>)GetValue(PointsProperty);
        set => SetValue(PointsProperty, value);
    }

    public PieChart() => InitializeComponent();
}

Open your MainPage.xaml that was added when you created a new .NET Maui project and assign these chart values to its binding context. In your mobile app, you should set the binding context as a ViewModel but this is the simplest way to do this without a ViewModel.

public MainPage()
{
    InitializeComponent();

    BindingContext = new Dictionary<string, float>()
    {
        {"Apples",25},
        {"Bananas",13},
        {"Strawberries",25},
        {"Blueberries", 53},
        {"Oranges", 14},
        {"Grapes", 52},
        {"Watermelons", 15},
        {"Pears",34 },
        {"Cantalopes", 67},
        {"Citrus",53 },
        {"Starfruit", 43},
        {"Papaya", 22},
        {"Papassya", 22},
    };
}

Now that you have chart data and a pie chart control all that’s left to do is add the control to your client code. I will be adding three charts side-by-side, so I use the new HorizontalStackLayout control.

<ContentPage iOS:Page.UseSafeArea="True"
             x:Class="MauiCharts.MainPage"
             xmlns:charts="clr-namespace:MauiCharts.Charts"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:iOS="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls">
    <HorizontalStackLayout>
        <charts:PieChart Points="{Binding .}" />
    </HorizontalStackLayout>
</ContentPage>

Build a Scrollable Gradient Bar Chart

When creating this bar chart, I took into account that more than likely there will be a lot of data to display in the chart. To accommodate this I gave this chart the ability to scroll horizontally, but there’s a problem. In Xamarin using SkiaSharp the canvas had an OnTouch method we could use to handle user touch events but in Maui Graphics I have not see such a capability. To work around this I add a Slider to the bar chart UI. This slider has a value from zero to one and we will use it to move points on the canvas from left to right.

Just like before we will start with our IDrawable for the bar chart control. Add a new BarChart folder to your Charts folder and create a BarChartDrawable class.

internal class BarChartDrawable : View, IDrawable
{
    public Dictionary<string, float> Points
    {
        get => _points;
        set
        { 
            _points = value;
            OnPropertyChanged();
        }
    }

    public double XAxisScale
    {
        get => _xAxisScale;
        set 
        { 
            _xAxisScale = value;
            OnPropertyChanged();
        }
    }

    public void Draw(ICanvas canvas, RectangleF dirtyRect)
    {
        const int BAR_WIDTH = 20;

        canvas.FontColor = Color.FromArgb("#7F2CF6");

        _chartWidth = dirtyRect.Width;

        //If the slider was moved then change x axis for the first bar
        if (XAxisScale != XAxisScaleOrigin)
            _firstBarXAxis += (float)(XAxisScale - XAxisScaleOrigin) * _chartWidth * -1;

        var barXAxis = _firstBarXAxis;
        //passing RGB ints to constructor does not work, should I submit a PR??
        var transparentMauiPurpleColor = Color.FromRgba(178, 127, 255, 0);
        var mauiPurpleColor = Color.FromRgb(178, 127, 255);

        var linearGradientPaint = new LinearGradientPaint
        {
            StartColor = mauiPurpleColor,
            EndColor = transparentMauiPurpleColor,
            StartPoint = new Point(0.5, 0),
            EndPoint = new Point(0.5, 1)
        };

        canvas.SetFillPaint(linearGradientPaint, dirtyRect);

        for (var i = 0; i < Points.Count; i++)
        {
            var point = Points.ElementAt(i);
            var barHeight = dirtyRect.Height - (dirtyRect.Height * (point.Value / Max) * BarScale);

            //Draw bars
            canvas.FillRectangle(barXAxis, barHeight, BAR_WIDTH, dirtyRect.Height - barHeight);

            //Draw text
            canvas.DrawString(point.Key, barXAxis, barHeight - 20, HorizontalAlignment.Center);
            barXAxis += BAR_WIDTH + 20;
        }

        XAxisScaleOrigin = XAxisScale;
    }

    public float Max;
    public float BarScale = 0.0f;
    public double XAxisScaleOrigin;
    public bool ChartsLoading = true;

    private float _chartWidth;
    private double _xAxisScale;
    private float _firstBarXAxis = 20.0f;
    private Dictionary<string, float> _points;
}
  • Points: A dictionary of bar titles (key), and bar integer values (value); one for each bar.
  • XAxisScale: A decimal value that indicates how far the user has moved the slider control. Every time its value changes the chart is redrawn from the GraphicsView, which we will get to in a bit. The code will check if the X-axis has changed from its previous value and if it has changed the add or subtract pixels from the first bars horizontal X-axis. Since all bars are positioned based on the first bar this in-turn moves the entire chart.
  • LinearGradientPaint: This paint is used to create a linear gradient effect. The StartPoint and EndPoint values determine the StartColor and EndColor positions and will only work with point values from zero to one. We set the X-axis to 0.5 for both colors so that the gradient is perfectly vertical.

Add a BarChartGraphicsView class to the BarChart folder and paste in the code below.

internal class BarChartGraphicsView : GraphicsView
{
    public static readonly BindableProperty XAxisScaleProperty = BindableProperty.Create(nameof(XAxisScale),
    typeof(double),
    typeof(BarChartGraphicsView),
    0.0,
    propertyChanged: (b, o, n) => {
        var graphicsView = ((BarChartGraphicsView)b);
        graphicsView.BarChartDrawable.XAxisScale = Convert.ToSingle(n);
        graphicsView.Invalidate();
    });

    public double XAxisScale
    {
        get => (double)GetValue(XAxisScaleProperty);
        set => SetValue(XAxisScaleProperty, value);
    }

    public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Points),
        typeof(Dictionary<string, float>),
        typeof(BarChartGraphicsView),
        new Dictionary<string, float>(),
        propertyChanged: (bindable, oldValue, newValue) =>
        {
            var chartView = (BarChartGraphicsView)bindable;

            chartView.BarChartDrawable.Max = chartView.Points?.Select(x => x.Value).Max() * 1.1f ?? 0.0f;

            //You can add/remove new data points which will redraw bar chart
            chartView.BarChartDrawable.Points = (Dictionary<string, float>)newValue;
        });

    public Dictionary<string, float> Points
    {
        get => (Dictionary<string, float>)GetValue(PointsProperty);
        set => SetValue(PointsProperty, value);
    }

    public BarChartGraphicsView()
    {
        Drawable = BarChartDrawable;

        LoadChartAnimation();
    }

    /// <summary>
    /// Animates bars from 1/30 scale over 1 second
    /// </summary>
    public void LoadChartAnimation()
    {
        for (var i = 0; i <= 30; i++)
        {
            BarChartDrawable.BarScale = i / 30f;
            Invalidate();
            Task.Delay(33);
        }
        BarChartDrawable.ChartsLoading = false;
    }

    public BarChartDrawable BarChartDrawable = new BarChartDrawable();
}

The XAxisScale property will assign the XAxisScale property of the BarChartDrawable control then redraw the canvas. As you saw previously the drawable uses this value to move the bar positions from left to right. There is also a Points property that is responsible for assigning points for the bars themselves. It also sets a Max property which is the maximum amount of points a bar can scale to. We multiple the highest value in the points collection by 1.1 to give the tallest bar a bit of head room. Finally, there is a LoadChartAnimation that will raise the bars from the ground when the page is loaded. Because our BarChartDrawable class multiplies bar values * scale we can grow the bars. The scale value increases by 1/30th. It does this 30 times every 30th of a second until one second has passed.

Now we can go ahead and create a UI control to render our BarChartGraphicsView and the slider. Create a new MAUI ContentPage (XAML) and change its base class to be a StackLayout.

<StackLayout x:Class="MauiCharts.Charts.BarChart"
             xmlns:charts="clr-namespace:MauiCharts.Charts"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <charts:BarChartGraphicsView x:Name="Chart"
                                 WidthRequest="200"
                                 HeightRequest="500"
                                 Points="{Binding .}"/>
    <Slider Minimum="0"
            Maximum="1"
            WidthRequest="200"
            Margin="30,0,30,0"
            ThumbColor="Purple"
            MinimumTrackColor="Violet"
            MaximumTrackColor="#7F2CF6"
            HorizontalOptions="FillAndExpand"
            Value="{Binding Source={x:Reference Chart}, Path=XAxisScale}"/>
</StackLayout>

In the code behind add a Points BindableProperty so that points can be assigned from client XAML.

public partial class BarChart : StackLayout
{
    public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Points),
            typeof(Dictionary<string, float>),
            typeof(BarChart),
            new Dictionary<string, float>(),
            propertyChanged: async (bindable, oldValue, newValue) =>
            {
                var chartView = ((BarChart)bindable);

                //Give the heighest bar a little head room for aesthetics
                chartView.Chart.BarChartDrawable.Max = chartView.Points?.Select(x => x.Value).Max() * 1.1f ?? 0.0f;

                //Set the points from XAML to component
                chartView.Chart.BarChartDrawable.Points = (Dictionary<string, float>)newValue;
            });

    public Dictionary<string, float> Points
    {
        get => (Dictionary<string, float>)GetValue(PointsProperty);
        set => SetValue(PointsProperty, value);
    }

    public BarChart() => InitializeComponent();
}

Everything should work perfectly now. Add the BarChart to the MainPage XAML. If you have any issues with BarChart during XAML compilation, it’s likely an ambiguous namespace issue. For all of the charts we develop in the excercise you want to make sure that XAML and C# code is all in the Charts namespace. For example, If you’re BarChart.xaml is in the Charts.BarChart namespace you will not be able to call it from client code using <charts:BarChart /> because of the ambiguity.

<ContentPage iOS:Page.UseSafeArea="True"
             x:Class="MauiCharts.MainPage"
             xmlns:charts="clr-namespace:MauiCharts.Charts"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:iOS="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls">
    <HorizontalStackLayout>
        <charts:BarChart Points="{Binding .}" />
        <charts:PieChart Points="{Binding .}" />
    </HorizontalStackLayout>
</ContentPage>

Scrollable Gradient 2D Line Chart

By now you should be getting the hang of how you can create 2D controls using the Maui.Graphics library. Pie and bar charts are nice, but they do not do a great job of helping the user visualize data over time. We need a linear graph to do this and just like the bar chart, we will give this chart horizontal scrolling abilities through the use of a Slider control. Add a new LineChart folder to the Charts folder as we will be adding our line chart related code here. Just like we did before we will start with the Drawable so add a LineChartDrawable file to this folder.


using GradientStop = Microsoft.Maui.Graphics.GradientStop;

namespace MauiCharts.Charts
{
    internal class LineChartDrawable : View, IDrawable
    {
        public Dictionary<string, float> Points
        {
            get => _points;
            set
            {
                _points = value;
                OnPropertyChanged();
            }
        }

        public double XAxisScale
        {
            get => _xAxisScale;
            set
            {
                _xAxisScale = value;
                OnPropertyChanged();
            }
        }

        public LineChartDrawable() => VerticalOptions = LayoutOptions.FillAndExpand;

        public void Draw(ICanvas canvas, RectangleF dirtyRect)
        {
            canvas.ResetState();

            const string purple = "#7F2CF6";
            const int POINT_SEGMENT_WIDTH = 100;
            canvas.StrokeColor = Color.FromArgb(purple);
            canvas.FontColor = Color.FromArgb(purple);
            canvas.FontSize = 16;

            //If the slider was moved then change x axis for the first bar
            if (XAxisScale != XAxisScaleOrigin)
                _firstPointXAxis += (float)(XAxisScale - XAxisScaleOrigin) * _lastPointXAxis * -1;

            var pointXAxis = _firstPointXAxis;
            var linearPath = new PathF();

            var transparentMauiPurpleGradientStop = new GradientStop(0.0f, Color.FromRgba(178, 127, 255,125));
            var mauiPurpleGradientStop = new GradientStop(0.27f, Color.FromRgba(235, 222, 255,0));
            var linearGradientPaint = new LinearGradientPaint
            {
                EndPoint = new Point(0, 1),
                StartPoint = new Point(0, 0),
                GradientStops = new GradientStop[] { transparentMauiPurpleGradientStop, mauiPurpleGradientStop }
            };
            
            //Generate path
            for (var i = 0; i < Points.Count; i++)
            {
                var point = Points.ElementAt(i);
                var yAxis = dirtyRect.Height - (dirtyRect.Height * (point.Value / Max));

                if (i == 0)
                {
                    linearPath.MoveTo(new PointF(pointXAxis, yAxis));
                }
                else
                {
                    linearPath.LineTo(new PointF(pointXAxis, yAxis));
                }

                var isLastDataPoint = i == Points.Count - 1;

                //Draw text
                //TODO where did MeasureText go?
                var pointText = $"{point.Key}: {point.Value}";
                canvas.DrawString(pointText,
                    pointXAxis + 50,
                    yAxis - 10,
                    i <= Points.Count - 2 
                    ? HorizontalAlignment.Right
                    : HorizontalAlignment.Left);

                //Remember last point x axis
                if (isLastDataPoint)
                    _lastPointXAxis = pointXAxis;

                //Move x axis to next point
                pointXAxis += POINT_SEGMENT_WIDTH + 20;
            }

            //canvas.SetFillPaint(linearGradientPaint, dirtyRect);
            canvas.SetFillPaint(linearGradientPaint, new RectangleF(0.0f, dirtyRect.Height - 100,dirtyRect.Width, dirtyRect.Height - 100));

            //Draw line chart
            canvas.DrawPath(linearPath);

            //Connect bottom of the line chart
            linearPath.LineTo(new PointF(_lastPointXAxis, dirtyRect.Height));
            linearPath.LineTo(new PointF(0, dirtyRect.Height));

            linearPath.Close();

            //Fill chart with gradient
            canvas.FillPath(linearPath);

            //Remember selected x axis
            XAxisScaleOrigin = XAxisScale;
        }

        public float Max;
        public double XAxisScaleOrigin;

        private double _xAxisScale;
        private float _lastPointXAxis;
        private float _firstPointXAxis = 0.0f;
        private Dictionary<string, float> _points;
    }
}
  • Points: A collection of titles (key), and bar integer values (value); one for each point on the line chart.
  • XAxisScale: A decimal value that indicates how far the user has moved the slider control. Every time its value changes the chart is redrawn from the GraphicsView, which we will get to in a bit. The code will check if the X-axis has changed from its previous value and if it has changed the add or subtract pixels from the first bars horizontal X-axis. Since all bars are positioned based on the first bar this in-turn moves the entire chart.
  • GradientStops: This control takes a different approach to creating a gradient. Instead of assigning StartColor & EndColor to the LinearGradientPaint you can add color points across the gradient axis. This approach is more flexible because it allows us to add as many colors as we like to a gradient. We do not need to do this because we only have the two colors however, I wanted to try this so I could have a better understanding of the new features in Maui.Graphics.
  • MoveTo: Indicates that this coordinate is the start of our path
  • LineTo: The next coordinate stopping point in the path.
  • Close: Connects the starting point and the last coordinate together, closing the path.

Create a LineChartGraphicsView class inside the LineChart folder and add the following code. This GraphicsView control is very similar to the BarChartGraphicsView control except it does not have the LoadAnimation method that the bar chart has. The XAxisScaleProperty will be bound to the Slider and every time its value changes it will redraw the line chart and use its value to position pixels along-side the line chart, creating a horizontal scrolling effect. The Points property is there to add numerical points to the component. It sets a Max value that is the maximum value of the Y-axis. It gets the highest value in the collection of points and multiplies it by 1.1 to give the highest point on the chart a bit of room up top.

internal class LineChartGraphicsView : GraphicsView
{
    public static readonly BindableProperty XAxisScaleProperty = BindableProperty.Create(nameof(XAxisScale),
    typeof(double),
    typeof(LineChartGraphicsView),
    0.0,
    propertyChanged: (b, o, n) => {
        var graphicsView = ((LineChartGraphicsView)b);
        graphicsView.LineChartDrawable.XAxisScale = Convert.ToSingle(n);
        graphicsView.Invalidate();
    });

    public double XAxisScale
    {
        get => (double)GetValue(XAxisScaleProperty);
        set => SetValue(XAxisScaleProperty, value);
    }

    public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Points),
        typeof(Dictionary<string, float>),
        typeof(LineChartGraphicsView),
        new Dictionary<string, float>(),
        propertyChanged: async (bindable, oldValue, newValue) =>
        {
            var chartView = ((LineChartGraphicsView)bindable);

            chartView.LineChartDrawable.Max = chartView.Points?.Select(x => x.Value).Max() * 1.1f ?? 0.0f;
            chartView.LineChartDrawable.Points = (Dictionary<string, float>)newValue;
        });

    public Dictionary<string, float> Points
    {
        get => (Dictionary<string, float>)GetValue(PointsProperty);
        set => SetValue(PointsProperty, value);
    }

    public LineChartGraphicsView() => Drawable = LineChartDrawable;

    public LineChartDrawable LineChartDrawable = new LineChartDrawable();
}

Next we need to add a LineChart view to the LineChart folder. Remember it is a Maui ContentPage and you will have to remove the LineChart string from the namespace to avoid namespace ambiguity issues.

<StackLayout x:Class="MauiCharts.Charts.LineChart"
             xmlns:charts="clr-namespace:MauiCharts.Charts"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <charts:LineChartGraphicsView x:Name="Chart"
                                  WidthRequest="600"
                                  HeightRequest="500"
                                  Points="{Binding .}"/>
    <Slider Minimum="0"
            Maximum="1"
            WidthRequest="600"
            Margin="30,0,30,0"
            ThumbColor="Purple"
            MinimumTrackColor="Violet"
            MaximumTrackColor="#7F2CF6"
            HorizontalOptions="FillAndExpand"
            Value="{Binding Source={x:Reference Chart}, Path=XAxisScale}"/>
</StackLayout>

In the code-behind of this component we will inherit from StackLayout and add the Points BindableProperty so that values can be assigned from the clients binding.

public partial class LineChart : StackLayout
{
    public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Points),
            typeof(Dictionary<string, float>),
            typeof(LineChart),
            new Dictionary<string, float>(),
            propertyChanged: async (bindable, oldValue, newValue) =>
            {
                var chartView = ((LineChart)bindable);

                //Give the heighest bar a little head room for aesthetics
                chartView.Chart.LineChartDrawable.Max = chartView.Points?.Select(x => x.Value).Max() * 1.1f ?? 0.0f;

                //Set the points from XAML to component
                chartView.Chart.LineChartDrawable.Points = (Dictionary<string, float>)newValue;
            });

    public Dictionary<string, float> Points
    {
        get => (Dictionary<string, float>)GetValue(PointsProperty);
        set => SetValue(PointsProperty, value);
    }
    public LineChart() => InitializeComponent();
}

This new LineChart component is all set and ready to be added to your UI. In the MainPage.xaml add a reference to LineChart and bind its Points value and your chart will now show up in your mobile view.

<ContentPage iOS:Page.UseSafeArea="True"
             x:Class="MauiCharts.MainPage"
             xmlns:charts="clr-namespace:MauiCharts.Charts"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:iOS="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls">
    <HorizontalStackLayout>
        <charts:LineChart Points="{Binding .}" />
        <charts:BarChart Points="{Binding .}" />
        <charts:PieChart Points="{Binding .}" />
    </HorizontalStackLayout>
</ContentPage>

Summary

Congratulations! You have learned how to create three graphs in .NET Maui, you’re a Rockstar! I have a few key take-aways from this excercise.

  • There is still an issue where the Draw method does not fire unless a Height & Width are assigned to the GraphicsView.
  • Where did MeasureText go?
  • When I used Hex values for RGBA colors the paths did not render.
  • We need a replacement for OnTouch in Maui.Graphics

I hope that you enjoyed learning how to build cool things in .NET Maui with me. Feel free to use this controls in your application and customize them anyway you see fit.

GitHub for .NET Maui charts

GitHub for Xamarin with SkiaSharp charts