Visual Elements

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

This web site wraps every sample using a ContentPage instance, but LiveCharts controls can be used inside any container.

sample image

We can add custom elements to a chart using the VisualElements property, this property is of type IEnumerable<ChartElement>, the library provides the Visual class, this class inherits from ChartElement and handles animations, the PointerDown event and the creation and destruction of the drawn geometries in the canvas.

In the next example, we create a CartesianChart, this chart contains multiple visual elements, each visual element is defined below in this article:

View Model

using System.Collections.Generic;
using LiveChartsCore;
using LiveChartsCore.Kernel;

namespace ViewModelsSamples.General.VisualElements;

public class ViewModel
{
    public IEnumerable<ChartElement> VisualElements { get; set; }

    public ISeries[] Series { get; set; }

    public ViewModel()
    {
        VisualElements = [
            new RectangleVisual(),
            new ScaledRectangleVisual(),
            new PointerDownAwareVisual(),
            new SvgVisual(),
            new CustomVisual(),
            new AbsoluteVisual(),
            new StackedVisual(),
            new TableVisual(),
            new ContainerVisual(),
        ];

        // no series are needed for this example
        Series = [];
    }
}

XAML

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Class="MauiSample.General.VisualElements.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.VisualElements;assembly=ViewModelsSamples">

    <ContentPage.BindingContext>
        <vms:ViewModel/>
    </ContentPage.BindingContext>

    <lvc:CartesianChart
        Series="{Binding Series}"
        VisualElements="{Binding VisualElements}"
        ZoomMode="X">
    </lvc:CartesianChart>

</ContentPage>

Basic sample

In the next example, we define the RectangleVisual class, this class inherits from Visual, then we override the Measure method and the DrawnElement property.

using LiveChartsCore;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class RectangleVisual : Visual
{
    protected override RectangleGeometry DrawnElement { get; } =
        new RectangleGeometry { Fill = new SolidColorPaint(SKColors.Red) };

    protected override void Measure(Chart chart)
    {
        DrawnElement.X = 100;
        DrawnElement.Y = 100;
        DrawnElement.Width = 40;
        DrawnElement.Height = 40;
    }
}

In the Measure method, we must define the properties of the DrawnElement, this method will be called every time the chart is measured; Now for the DrawnElement property, we override the return type using the covariant returns feature, this means that we can override the type of the DrawnElement property, but the new type must implement IDrawnElement, this interface is implement by any object drawn by LiveCharts, you can use the geometries already defined in the library (here is a list) or you can define your own geometries as soon as they implement this interface.

Scaled shapes

Normally, we need to scale the drawn shapes based on the chart data, you can use the ScaleDataToPixels method to do so:

using LiveChartsCore;
using LiveChartsCore.Drawing;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class ScaledRectangleVisual : Visual
{
    protected override RectangleGeometry DrawnElement { get; } =
        new RectangleGeometry { Fill = new SolidColorPaint(SKColors.Red) };

    protected override void Measure(Chart chart)
    {
        var cartesianChart = (ICartesianChartView)chart.View;

        // use the ScaleDataToPixels function to scale data.
        var locationInDataScale = new LvcPointD(5, 5);
        var locationInPixels = cartesianChart.ScaleDataToPixels(locationInDataScale);

        DrawnElement.X = (float)locationInPixels.X;
        DrawnElement.Y = (float)locationInPixels.Y;
        DrawnElement.Width = 40;
        DrawnElement.Height = 40;
    }
}

PointerDown event

You can subscribe to the PointerDown event to detect when the user pointer goes down on the element.

using System.Diagnostics;
using LiveChartsCore;
using LiveChartsCore.Kernel.Events;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class PointerDownAwareVisual : Visual
{
    public PointerDownAwareVisual()
    {
        PointerDown += OnPointerDown;
    }

    private void OnPointerDown(IInteractable visual, VisualElementEventArgs visualElementsArgs)
    {
        var location = visualElementsArgs.PointerLocation;
        Trace.WriteLine($"Pointer down at {location.X}, {location.Y}");
    }

    protected override RectangleGeometry DrawnElement { get; } =
        new RectangleGeometry { Fill = new SolidColorPaint(SKColors.Red) };

    protected override void Measure(Chart chart)
    {
        DrawnElement.X = 150;
        DrawnElement.Y = 100;
        DrawnElement.Width = 40;
        DrawnElement.Height = 40;
    }
}

Svg shapes

Use the VariableSVGPathGeometry as the DrawnElement to draw svg paths:

using LiveChartsCore;
using LiveChartsCore.Drawing;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class SvgVisual : Visual
{
    protected override VariableSVGPathGeometry DrawnElement { get; } =
        new VariableSVGPathGeometry
        {
            Stroke = new SolidColorPaint(SKColors.Blue),
            Path = SKPath.ParseSvgPathData(SVGPoints.Star)
        };

    protected override void Measure(Chart chart)
    {
        DrawnElement.X = 200;
        DrawnElement.Y = 100;
        DrawnElement.Width = 40;
        DrawnElement.Height = 40;
    }
}

Custom IDrawnElement

The easiest way is to inherit from DrawnGeometry, this class implements IDrawnElement and also animates all of its properties; In the next example we inherit from BoundedDrawnGeometry it only adds the Width and Height properties to the DrawnGeometry class.

sample image
using LiveChartsCore.Drawing;
using LiveChartsCore.SkiaSharpView.Drawing;

namespace ViewModelsSamples.General.VisualElements;

public class CustomSkiaShape : BoundedDrawnGeometry, IDrawnElement<SkiaSharpDrawingContext>
{
    public void Draw(SkiaSharpDrawingContext context)
    {
        var canvas = context.Canvas;
        var paint = context.ActiveSkiaPaint;

        var x = X + Width / 2f;
        var y = Y + Height / 2f;
        var r = (float)Width / 2f;

        canvas.DrawCircle(x, y, r, paint);
        canvas.DrawCircle(x, y, r * 0.75f, paint);
        canvas.DrawCircle(x, y, r * 0.5f, paint);
    }
}

Finally, we need to define a Visual that uses this geometry as the DrawnElement:

using LiveChartsCore;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class CustomVisual : Visual
{
    protected override CustomSkiaShape DrawnElement { get; } =
        new CustomSkiaShape { Stroke = new SolidColorPaint(SKColors.Red) };

    protected override void Measure(Chart chart)
    {
        DrawnElement.X = 250;
        DrawnElement.Y = 100;
        DrawnElement.Width = 40;
        DrawnElement.Height = 40;
    }
}

If you need to define your own properties and these properties must be animated, you must use the MotionProperty<T> class, please see the draw on canvas sample for more info.

Layouts

LiveCharts provides a small layout framework, this is used by the library to render the tooltips and legends, Layouts also implement IDrawnElement, but they are not drawn in the UI, instead they just define the coordinates of the elements inside when an element is added to a layout, the coordinates of these elements are relative to the layout, here is a list of the layouts defined in the library.

Container Layout

A container is just a shape that can host other drawn elements as the content of this shape, a container takes the size of its content, and can set the Fill and Stroke properties of this shape. an example in the library is the default tooltip, it is of type Container<PopUpGeometry> then it sets the Geometry.Fill to define the background of the tooltip.

using LiveChartsCore;
using LiveChartsCore.Drawing;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Drawing.Layouts;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class ContainerVisual : Visual
{
    private readonly Container _container;

    public ContainerVisual()
    {
        _container = new Container
        {
            Content = new LabelGeometry
            {
                Text = "Hello",
                TextSize = 20,
                Padding = new(10),
                Paint = new SolidColorPaint(SKColors.Black),
                VerticalAlign = Align.Start,
                HorizontalAlign = Align.Start
            }
        };

        _container.Geometry.Stroke = new SolidColorPaint(SKColors.Black, 3);
        _container.Geometry.Fill = new SolidColorPaint(SKColors.LightGray);
    }

    protected override Container DrawnElement => _container;

    protected override void Measure(Chart chart)
    {
        DrawnElement.X = 500;
        DrawnElement.Y = 100;
    }
}

Absolute Layout

Used to place children on its own coordinate system, all the children X and Y coordinates are relative to the Layout position, the layout takes the size of the largest element in the children collection. For example in the next case, we place the place the RectangleGeometryin the 0,0 coordinate [in the layout system] and theLabelGeometry` in the 10,0 coordinate.

sample image
using LiveChartsCore;
using LiveChartsCore.Drawing;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Drawing.Layouts;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class AbsoluteVisual : Visual
{
    protected override AbsoluteLayout DrawnElement { get; } =
        new AbsoluteLayout
        {
            // X and Y coordinates are relative to the parent
            Children = [
                new RectangleGeometry
                {
                    X = 0,
                    Y = 0,
                    Width = 40,
                    Height = 40,
                    Fill = new SolidColorPaint(SKColors.Gray)
                },
                new LabelGeometry
                {
                    X = 10,
                    Y = 10,
                    Text = "Hello",
                    TextSize = 10,
                    HorizontalAlign = Align.Start,
                    VerticalAlign = Align.Start,
                    Paint = new SolidColorPaint(SKColors.Black)
                }
            ]
        };

    protected override void Measure(Chart chart)
    {
        DrawnElement.X = 300;
        DrawnElement.Y = 100;
        DrawnElement.Width = 40;
        DrawnElement.Height = 40;
    }
}

Stacked Layout

Stacks IDrawnElement objects in vertical or horizontal order.

sample image
using LiveChartsCore;
using LiveChartsCore.Drawing;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Drawing.Layouts;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class StackedVisual : Visual
{
    protected override StackLayout DrawnElement { get; } =
        new StackLayout
        {
            // X and Y coordinates are relative to the parent
            Orientation = ContainerOrientation.Vertical,
            Children = [
                new CircleGeometry
                {
                    Width = 40,
                    Height = 40,
                    Fill = new SolidColorPaint(SKColors.CadetBlue)
                },
                new RectangleGeometry
                {
                    Width = 40,
                    Height = 40,
                    Fill = new SolidColorPaint(SKColors.DeepSkyBlue)
                },
                new DiamondGeometry
                {
                    Width = 40,
                    Height = 40,
                    Fill = new SolidColorPaint(SKColors.LightSteelBlue)
                }
            ]
        };

    protected override void Measure(Chart chart)
    {
        DrawnElement.X = 350;
        DrawnElement.Y = 100;
    }
}

Table Layout

Uses a grid system to place IDrawnElement objects.

sample image
using LiveChartsCore;
using LiveChartsCore.Drawing;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Drawing.Layouts;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.VisualElements;
using SkiaSharp;

namespace ViewModelsSamples.General.VisualElements;

public class TableVisual : Visual
{
    protected override TableLayout DrawnElement { get; } =
        new TableLayout
        {
            Cells = [
                new(0, 0, GetLabel("A")),
                new(0, 1, GetLabel("-")),
                new(0, 2, GetLabel("B")),
                new(1, 0, GetLabel("-")),
                new(1, 1, GetLabel("C")),
                new(1, 2, GetLabel("-"))
            ],
        };

    protected override void Measure(Chart chart)
    {
        DrawnElement.X = 400;
        DrawnElement.Y = 100;
    }

    private static LabelGeometry GetLabel(string text) =>
        new()
        {
            Text = text,
            TextSize = 20,
            Padding = new(10),
            Paint = new SolidColorPaint(SKColors.Black),
            VerticalAlign = Align.Start,
            HorizontalAlign = Align.Start
        };
}

Update on property change

To do...