This article adds geometries directly to the canvas, this is intended to explain how geometries and animations are handled in the library, but in general the recommended way to draw a custom element in the chart is to use the Visual class, for more info please see the visual elements article. ::

Draw On The Chart

This sample uses C# 12 features, it also uses features from the CommunityToolkit.Mvvm package, you can learn more about it here.

sample image

We can directly draw on the canvas to create custom shapes or effects, by default LiveCharts uses SkiaSharp to render the controls, this means that you can use all the SkiaSharp API to draw on the canvas, you can find more information about SkiaSharp here.

In the next example we use the UpdateStarted command/event in the CartesianChart, this command/event is raised every time the control is measured, a LiveCharts control is measured when the data or the control size change.

View model

using System;
using CommunityToolkit.Mvvm.Input;
using LiveChartsCore;
using LiveChartsCore.Drawing;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Events;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.Motion;
using LiveChartsCore.SkiaSharpView.Drawing;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;

namespace ViewModelsSamples.General.DrawOnCanvas;

public partial class ViewModel
{
    private readonly MotionGeometry _geometry = new() { Stroke = new SolidColorPaint(SKColors.Blue) };
    private bool _isInitialized;
    private bool _isBigCircle = true;

    [RelayCommand]
    public void ChartUpdated(ChartCommandArgs args)
    {
        var chartView = (ICartesianChartView)args.Chart;

        // lets convert the point (5, 5) in the chart values scale to pixels
        var locationInChartValues = new LvcPointD(5, 5);
        var locationInPixels = chartView.ScaleDataToPixels(locationInChartValues);

        _geometry.X = (float)locationInPixels.X;
        _geometry.Y = (float)locationInPixels.Y;
        // lets toggle the diameter of the circle between 20 and 70
        _geometry.Diameter = (_isBigCircle = !_isBigCircle) ? 70 : 20;

        if (!_isInitialized)
        {
            chartView.Core.Canvas.AddGeometry(_geometry);
            _geometry.Animate(
                new Animation(EasingFunctions.BounceOut, TimeSpan.FromMilliseconds(800)));
            _isInitialized = true;
        }
    }
}

public class MotionGeometry : BoundedDrawnGeometry, IDrawnElement<SkiaSharpDrawingContext>
{
    // use Motion properties to animate the geometry
    private readonly FloatMotionProperty _diameter;

    public MotionGeometry()
    {
        _diameter = RegisterMotionProperty(new FloatMotionProperty(nameof(Diameter), 0));
    }

    public float Diameter
    {
        get => _diameter.GetMovement(this);
        set => _diameter.SetMovement(value, this);
    }

    public void Draw(SkiaSharpDrawingContext context)
    {
        // we can use SkiaSharp here to draw anything we need // mark
        // https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/basics/ // mark

        // because we inherited from Geometry, this class already contains the X, Y
        // and some other motion properties that we can use.

        var paint = context.ActiveSkiaPaint;
        context.Canvas.DrawCircle(X, Y, Diameter, paint);
    }
}


XAML

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Class="MauiSample.General.DrawOnCanvas.View"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.Maui;assembly=LiveChartsCore.SkiaSharpView.Maui"
             xmlns:vms="clr-namespace:ViewModelsSamples.General.DrawOnCanvas;assembly=ViewModelsSamples"
             >
    <ContentPage.BindingContext>
        <vms:ViewModel/>
    </ContentPage.BindingContext>
	<ContentPage.Content>
        <Grid>
            <lvc:CartesianChart UpdateStartedCommand="{Binding ChartUpdatedCommand}"/>
        </Grid>
    </ContentPage.Content>
</ContentPage>

In the previous case we inherited from Geometry, this class already contains some useful properties that we can use to set the location, rotation, opacity or transform of the geometry, you can find more information about the Geometry class here.

We override the OnDraw method to define the drawing logic of our custom geometry, in this case we are only drawing a circle based on the X, Y and Diameter properties.

These properties are not regular properties, they are a special type defined by LiveCharts, the MotionProperty type is what handles animations in LiveCharts. Every time we access a motion property we get the value of the property at the current time based on the IAnimatable animation.

We also created an instance of the SolidColorPaint class, this class defines how the geometry will be rendered on the canvas, in this case a blue color with a stroke width of 2, then we added our geometry to the paint (you can add multiple geometries), and we also added the paint to the canvas.

The user interface update cycle is the following:

  1. The chart control is invalidated (data changed or size changed), so the control is measured and with it the UpdateStarted command/event is raised.

  2. When the control is measured, we add Paints to the canvas, and geometries to the paints, this is only scheduling the drawing, nothing is rendered yet at this point.

  3. Now the control starts drawing all the paints and geometries in the canvas, this is when the OnDraw method is called.

  4. LiveCharts defines the MotionCanvas class, it redraws the user interface as all the animations complete. This means that the previous step is repeated multiple times per second (~60), so you must be careful when overriding the OnDraw method, you should only perform drawing operations there. LiveCharts will keep drawing until all animations are finished.

Articles you might also find useful: