Series events

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;

[ObservableObject]
public partial class ViewModel
{
    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>
        {
            Name = "Items sold per day",
            Values = data,
            TooltipLabelFormatter = point => $"{point.Model?.Name}, sold {point.Model?.SalesPerDay} items",
            DataLabelsPaint = new SolidColorPaint(new SKColor(30, 30, 30)),
            DataLabelsFormatter = point => $"{point.Model?.SalesPerDay} {point.Model?.Name}",
            DataLabelsPosition = DataLabelsPosition.End,
            Mapping = (fruit, point) =>
            {
                point.PrimaryValue = fruit.SalesPerDay; // use the SalesPerDay property in this series // mark
                point.SecondaryValue = point.Context.Index;
            }
        };

        // 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; }
}
using Eto.Forms;
using LiveChartsCore.SkiaSharpView.Eto;
using ViewModelsSamples.Events.Cartesian;

namespace EtoFormsSample.Events.Cartesian;

public class View : Panel
{
    /// <summary>
    /// Initializes a new instance of the <see cref="View"/> class.
    /// </summary>
    public View()
    {
        var viewModel = new ViewModel();

        var cartesianChart = new CartesianChart
        {
            Series = viewModel.Series,
        };

        Content = cartesianChart;
    }
}

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;

[ObservableObject]
public partial class ViewModel
{
    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,
            TooltipLabelFormatter = point => $"{point.Model?.Name} {point.Model?.Population} Million",
            Mapping = (city, point) =>
            {
                point.PrimaryValue = city.Population; // use the population property in this series // mark
                point.SecondaryValue = point.Context.Index;
            }
        };

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

    public ISeries[] Series { get; set; }

    [ICommand]
    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)
            // {
            //     ...
            // }
        }
    }
}
using Eto.Forms;
using LiveChartsCore.SkiaSharpView.Eto;
using ViewModelsSamples.Events.Polar;

namespace EtoFormsSample.Events.Polar;

public class View : Panel
{
    /// <summary>
    /// Initializes a new instance of the <see cref="View"/> class.
    /// </summary>
    public View()
    {
        var viewModel = new ViewModel();

        var cartesianChart = new PolarChart
        {
            Series = viewModel.Series,
        };

        Content = cartesianChart;
    }
}