Series events

The [ObservableObject], [ObservableProperty] and [RelayCommand] attributes come from the CommunityToolkit.Mvvm package, you can read more about it here.

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

In this example a column turns yellow when the pointer is above, then it turns red when the pointer goes down and finally restores the default paint when the pointer leaves.

sample image

View model

using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using LiveChartsCore;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.Measure;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;

namespace ViewModelsSamples.Events.Cartesian;

public partial class ViewModel : ObservableObject
{
    public ViewModel()
    {
        var data = new[]
        {
            new Fruit { Name = "Apple", SalesPerDay = 4, Stock = 6 },
            new Fruit { Name = "Orange", SalesPerDay = 6, Stock = 4 },
            new Fruit { Name = "Pinaple", SalesPerDay = 2, Stock = 2 },
            new Fruit { Name = "Potoato", SalesPerDay = 8, Stock = 4 },
            new Fruit { Name = "Lettuce", SalesPerDay = 3, Stock = 6 },
            new Fruit { Name = "Cherry", SalesPerDay = 4, Stock = 8 }
        };

        var salesPerDaysSeries = new ColumnSeries<Fruit>
        {
            Values = data,
            YToolTipLabelFormatter = point => $"{point.Model?.SalesPerDay} {point.Model?.Name}",
            DataLabelsPaint = new SolidColorPaint(new SKColor(30, 30, 30)),
            DataLabelsFormatter = point => $"{point.Model?.SalesPerDay} {point.Model?.Name}",
            DataLabelsPosition = DataLabelsPosition.End,
            // use the SalesPerDay property in this in the Y axis // mark
            // and the index of the fruit in the array in the X axis // mark
            Mapping = (fruit, index) => new(index, fruit.SalesPerDay)
        };

        // notice that the event signature is different for every series
        // use the  IDE intellisense to help you (see more bellow in this article). // mark
        salesPerDaysSeries.ChartPointPointerDown += OnPointerDown; // mark
        salesPerDaysSeries.ChartPointPointerHover += OnPointerHover; // mark
        salesPerDaysSeries.ChartPointPointerHoverLost += OnPointerHoverLost; // mark

        Series = new ISeries[] { salesPerDaysSeries };
    }

    public ISeries[] Series { get; set; }

    private void OnPointerDown(IChartView chart, ChartPoint<Fruit, RoundedRectangleGeometry, LabelGeometry>? point)
    {
        if (point?.Visual is null) return;
        point.Visual.Fill = new SolidColorPaint(SKColors.Red);
        chart.Invalidate(); // <- ensures the canvas is redrawn after we set the fill
        Trace.WriteLine($"Clicked on {point.Model?.Name}, {point.Model?.SalesPerDay} items sold per day");
    }

    private void OnPointerHover(IChartView chart, ChartPoint<Fruit, RoundedRectangleGeometry, LabelGeometry>? point)
    {
        if (point?.Visual is null) return;
        point.Visual.Fill = new SolidColorPaint(SKColors.Yellow);
        chart.Invalidate();
        Trace.WriteLine($"Pointer entered on {point.Model?.Name}");
    }

    private void OnPointerHoverLost(IChartView chart, ChartPoint<Fruit, RoundedRectangleGeometry, LabelGeometry>? point)
    {
        if (point?.Visual is null) return;
        point.Visual.Fill = null;
        chart.Invalidate();
        Trace.WriteLine($"Pointer left {point.Model?.Name}");
    }
}

Fruit.cs

namespace ViewModelsSamples.Events.Cartesian;

public class Fruit
{
    public string Name { get; set; } = string.Empty;
    public double SalesPerDay { get; set; }
    public int Stock { get; set; }
}

XAML

<UserControl
    x:Class="UnoWinUISample.Events.Cartesian.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.Events.Cartesian"
    mc:Ignorable="d">

    <UserControl.DataContext>
        <vms:ViewModel/>
    </UserControl.DataContext>
    <Grid>
        <lvc:CartesianChart
                Series="{Binding Series}"
                TooltipFindingStrategy="CompareOnlyX">
        </lvc:CartesianChart>
    </Grid>

</UserControl>

By using the Series events you can subscribe strongly typed method signatures, where the library knows the type of the visual, the type of the label and the data context we are drawing.

LiveCharts allows you to use any shape to represent a chart point (see custom svg point example), you can also plot any type you need for example in the example above we are plotting instances of the Fruit class, the library is able to keep events strongly typed, but it could be tricky to guess the signature since the it changes depending on the series type, the visual shape and the geometry.

Please use the IDE intellisense to complete the signature:

sample image

Notice how the IDE is able to detect that the first series is of type int (ScatterSeries<int>) while the second is of type double and is drawing RectangleGeometry instances to represent the visual points (ScatterSeries<double, RectangleGeometry>).

Using Events at the chart level

You could also detect the pointer down events/commands at the chart level but since the chart Series property is of type ISeries the library is not able to determine the type of the series and we lose the strongly typed chart points.

using System.Collections.Generic;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LiveChartsCore;
using LiveChartsCore.Kernel;
using LiveChartsCore.SkiaSharpView;

namespace ViewModelsSamples.Events.Polar;

public partial class ViewModel : ObservableObject
{
    public ViewModel()
    {
        var data = new[]
        {
            new City { Name = "Tokyo", Population = 4 },
            new City { Name = "New York", Population = 6 },
            new City { Name = "Seoul", Population = 2 },
            new City { Name = "Moscow", Population = 8 },
            new City { Name = "Shanghai", Population = 3 },
            new City { Name = "Guadalajara", Population = 4 }
        };

        var polarLineSeries = new PolarLineSeries<City>
        {
            Values = data,
            RadiusToolTipLabelFormatter = point => $"{point.Model?.Name} {point.Model?.Population} Million",
            // use the Population property in the Y axis
            // and the index of the city in the array as the X axis
            Mapping = (city, index) => new(index, city.Population)
        };

        Series = new ISeries[]
        {
            polarLineSeries,
            new PolarLineSeries<int> { Values = new[] { 6, 7, 2, 9, 6, 2 } },
        };
    }

    public ISeries[] Series { get; set; }

    [RelayCommand]
    public void DataPointerDown(IEnumerable<ChartPoint>? points)
    {
        if (points is null) return;

        // notice in the chart command we are not able to use strongly typed points
        // but we can cast the point.Context.DataSource to the actual type.

        foreach (var point in points)
        {
            if (point.Context.DataSource is City city)
            {
                Trace.WriteLine($"[chart.dataPointerDownCommand] clicked on {city.Name}");
                continue;
            }

            if (point.Context.DataSource is int integer)
            {
                Trace.WriteLine($"[chart.dataPointerDownCommand] clicked on number {integer}");
                continue;
            }

            // handle more possible types here...
            // if (point.Context.DataSource is Foo foo)
            // {
            //     ...
            // }
        }
    }
}
<UserControl
    x:Class="UnoWinUISample.Events.Polar.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.Events.Polar"
    mc:Ignorable="d">

    <UserControl.DataContext>
        <vms:ViewModel/>
    </UserControl.DataContext>
    <Grid>
        <lvc:PolarChart
                Series="{Binding Series}"
                DataPointerDownCommand="{Binding DataPointerDownCommand}"
                DataPointerDown="Chart_DataPointerDown">
        </lvc:PolarChart>
    </Grid>
</UserControl>

View code behind

using System.Collections.Generic;
using System.Diagnostics;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Sketches;
using ViewModelsSamples.Events.Polar;
using Microsoft.UI.Xaml.Controls;

namespace UnoWinUISample.Events.Polar;

public sealed partial class View : UserControl
{
    public View()
    {
        InitializeComponent();
    }

    private void Chart_DataPointerDown(
        IChartView chart,
        IEnumerable<ChartPoint> points)
    {
        // notice in the chart event we are not able to use strongly typed points
        // but we can cast the point.Context.DataSource to the actual type.

        foreach (var point in points)
        {
            if (point.Context.DataSource is City city)
            {
                Trace.WriteLine($"[chart.dataPointerDownEvent] clicked on {city.Name}");
                continue;
            }

            if (point.Context.DataSource is int integer)
            {
                Trace.WriteLine($"[chart.dataPointerDownEvent] clicked on number {integer}");
                continue;
            }

            // handle more possible types here...
            // if (point.Context.DataSource is Foo foo)
            // {
            //     ...
            // }
        }
    }
}