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# 13 preview features such as partial properties, it also uses features from the CommunityToolkit.Mvvm package, you can learn more about it here.
Pre-requisites
This example uses MotionProperties, this is a special type of property in the library that allows properties to animate
when the value changes. MotionProperties require a lot of metadata to work (similar to dependency or bindable properties)
but instead of manually writing all that boring code, install the LiveChartsGenerators NuGet package:
<PackageReference Include="LiveChartsGenerators" Version="1.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>analyzers</IncludeAssets>
</PackageReference>
Intro
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.Generators;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Events;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.SkiaSharpView.Drawing;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
namespace ViewModelsSamples.General.DrawOnCanvas;
// in this example, we use the UpdateStartedCommand to add our custom // mark
// geometry to the chart, then we use the PointerPressedCommand to change the //mark
// size of the geometry when the user clicks on it. // mark
public partial class ViewModel
{
private bool _isBig;
private readonly MotionGeometry _geometry = new()
{
Diameter = 20,
Fill = new SolidColorPaint(SKColors.Blue.WithAlpha(100)),
Stroke = new SolidColorPaint(SKColors.Blue, strokeWidth: 3)
};
[RelayCommand]
public void ChartUpdated(ChartCommandArgs args)
{
var chartView = (ICartesianChartView)args.Chart;
// lets place the geometry at (5, 5) in the chart values scale
var locationInChartValues = new LvcPointD(5, 5);
var locationInPixels = chartView.ScaleDataToPixels(locationInChartValues);
_geometry.X = (float)locationInPixels.X;
_geometry.Y = (float)locationInPixels.Y;
_geometry.Initialize(args);
}
[RelayCommand]
public void ChartPressed(PointerCommandArgs args)
{
_geometry.Diameter = _isBig ? 20 : 70;
_isBig = !_isBig;
args.Chart.Invalidate();
}
}
public partial class MotionGeometry : BoundedDrawnGeometry, IDrawnElement<SkiaSharpDrawingContext>
{
private bool _isInitialized;
// The MotionProperty attribute makes the property animatable, // mark
// both the property and the class must be marked as partial // mark
// partial properties were introduced in C# 13 // mark
// A motion property will animate any change in the property // mark
// according to the Animation defined in the Animate method. // mark
[MotionProperty]
public partial float Diameter { get; set; }
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 BoundedDrawnGeometry, 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);
}
public void Initialize(ChartCommandArgs args)
{
if (_isInitialized) return;
args.Chart.CoreChart.Canvas.AddGeometry(this);
var animation = new Animation(
easingFunction: EasingFunctions.BounceOut,
duration: TimeSpan.FromMilliseconds(800));
this.Animate(animation);
_isInitialized = true;
}
}
XAML
<UserControl
x:Class="AvaloniaSample.General.DrawOnCanvas.View"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:vms="using:ViewModelsSamples.General.DrawOnCanvas"
x:DataType="vms:ViewModel">
<UserControl.DataContext>
<vms:ViewModel/>
</UserControl.DataContext>
<lvc:CartesianChart
UpdateStartedCommand="{Binding ChartUpdatedCommand}"
PointerPressedCommand="{Binding ChartPressedCommand}"/>
</UserControl>
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
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:
The chart control is invalidated (data changed or size changed), so the control is measured and with it the
UpdateStartedcommand/event is raised.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.
Now the control starts drawing all the paints and geometries in the canvas, this is when the
OnDrawmethod is called.LiveCharts defines the
MotionCanvasclass, 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 theOnDrawmethod, you should only perform drawing operations there. LiveCharts will keep drawing until all animations are finished.