Draw On The Chart
The [ObservableObject]
, [ObservableProperty]
and [RelayCommand]
attributes come from the
CommunityToolkit.Mvvm package, you can read more about it
here.
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 changes 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.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
namespace ViewModelsSamples.General.DrawOnCanvas;
public partial class ViewModel
{
private SolidColorPaint? _paint;
private MotionGeometry? _geometry;
private bool _isBigCircle = true;
[RelayCommand]
public void ChartUpdated(ChartCommandArgs args)
{
var chartView = (ICartesianChartView<SkiaSharpDrawingContext>)args.Chart;
if (_geometry is null)
{
_geometry = new MotionGeometry();
_geometry.Animate(
new(EasingFunctions.BounceOut, TimeSpan.FromMilliseconds(800)));
}
if (_paint is null)
{
_paint = new SolidColorPaint(SKColors.Blue) { IsStroke = true, StrokeThickness = 2 };
_paint.AddGeometryToPaintTask(chartView.CoreCanvas, _geometry);
chartView.CoreCanvas.AddDrawableTask(_paint);
}
// 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 this is the first time we draw the geometry
// we can complete the animations.
// if (isNewGeometry) _motionGeometry.CompleteTransition();
}
}
public class MotionGeometry : Geometry
{
// 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 override void OnDraw(SkiaSharpDrawingContext context, SKPaint paint)
{
// 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.
context.Canvas.DrawCircle(X, Y, Diameter, paint);
}
protected override LvcSize OnMeasure(IPaint<SkiaSharpDrawingContext> paintTasks)
{
// you can measure the geometry here, this method is used when the geometry
// is used inside a layout, in this case it is not necessary.
return new();
}
}
XAML
<UserControl
x:Class="UnoWinUISample.General.DrawOnCanvas.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.WinUI"
xmlns:vms="using:ViewModelsSamples.General.DrawOnCanvas"
mc:Ignorable="d">
<UserControl.DataContext>
<vms:ViewModel/>
</UserControl.DataContext>
<lvc:CartesianChart UpdateStartedCommand="{Binding ChartUpdatedCommand}"></lvc:CartesianChart>
</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
UpdateStarted
command/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 is starts drawing all the paints and geometries in the canvas, this is when the
OnDraw
method is called.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 theOnDraw
method, you should only perform drawing operations there. LiveCharts will keep drawing until all animations are finished.