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.
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:
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)
// {
// ...
// }
}
}
}