~/overview/1.5.mappers.md

Introduction to custom types

You can plot anything in a chart as soon as you let the library how to handle that object, LiveCharts already supports the types short, int, long, float, double, decimal, their nullable versions short?, int?, long?, float?, double?, decimal?, and also some objects that are able to update automatically the changes to the UI like the ObservableValue, ObservablePoint (useful to specify both, X and Y), WeightedPoint (used in bubble charts), DateTimePoint (the X coordinate is of type DateTime), TimeSpanPoint (the X coordinate is of type TimeSpan), ObservablePolarPoint (used in polar charts) FinancialPoint and FinancialPointI (to create candlestick charts).

Imagine the case that we have a json file that contains the temperature of a CPU at a given time, we want to build a chart with that data.

[
  {
    "Time": 1,
    "Temperature": 65.65,
    "Unit": "Celcius"
  },
  {
    "Time": 5,
    "Temperature": 62.23,
    "Unit": "Celcius"
  },
  {
    "Time": 8,
    "Temperature": 85.12,
    "Unit": "Celcius"
  }
]

We can read that file, and deserialize it to an array of the TempSample class.

using var streamReader = new StreamReader("data.json");
var samples = JsonSerializer.Deserialize<TempSample[]>(streamReader.ReadToEnd());

// now lets build the chart 
var chart = new SKCartesianChart
{
    Width = 900,
    Height = 600,
    Series = new[]
    {
        new LineSeries<TempSample>
        {
            Values = samples
        }
    },
    XAxes = new[] { new Axis { Labeler = value => $"{value} seconds" } },
    YAxes = new[] { new Axis { Labeler = value => $"{value} °C" } }
};

chart.SaveImage("chart.png");

The code above will throw because LiveCharts need to know how to plot the TempSample class; we can teach LiveCharts how to handle the TempSample class by setting a Mapper or implementing IChartEntity in our TempSample class.

Mappers

Mappers are the easiest way but has a performance cost, a mapper is a method that takes both the instance (each TempSample in our data collection) and the point (that LiveCharts assigned) as parameters, inside this function we must specify the X and Y coordinates in our chart.

using var streamReader = new StreamReader("data.json");
var samples = JsonSerializer.Deserialize<TempSample[]>(streamReader.ReadToEnd());

// now we just build the chart 
var chart = new SKCartesianChart
{
    Width = 900,
    Height = 600,
    Series = new[]
    {
        new LineSeries<TempSample>
        {
            Values = samples,
            Mapping = (sample, chartPoint) => // mark
            { // mark
                // use the Temperature property in the Y axis // mark
                // and the Time property in the X axis // mark
                chartPoint.Coordinate = new(sample.Time, sample.Temperature);
                
                // sometimes it is useful to use the index of the instance in the array as the X coordinate: // mark
                // chartPoint.SecondaryValue = chartPoint.Index; // mark
            } // mark
        }
    },
    XAxes = new[] { new Axis { Labeler = value => $"{value} seconds" } },
    YAxes = new[] { new Axis { Labeler = value => $"{value} °C" } }
};

chart.SaveImage("chart.png");

Now it works! You can also register the mapper globally, this means that every time the TempSample class is used in a chart all over our application, the library will use the mapper we indicated.

// ideally this code must be placed where your application or view starts
LiveCharts.Configure(config =>
    config
        .HasMap<TempSample>(
            (sample, chartPoint) =>
            {
                chartPoint.PrimaryValue = sample.Temperature;
                chartPoint.SecondaryValue = sample.Time;
            }));

Global mappers are unique for a type, this means that every time a TempSample instance is in a chart, LiveCharts will use this mapper, if You register again a global mapper for the TempSample class, then the previous will be replaced by the new one. If the series specifies the Mapping property, then the global mapper will be ignored and instead it will use the series instance mapper.

See full Mapping sample

IChartEntity

The IChartEntity interface force our points to have a Coordinate, LiveCharts will use this property to build the plot, when the interface is implemented correctly, you will notice a considerable performance improvement, specially in large data sets.

Imagine the same case we used in the previous sample where we have a json file that contains the temperature of a CPU at a given time, we want to build a chart with that data.

[
  {
    "Time": 1,
    "Temperature": 65.65,
    "Unit": "Celcius"
  },
  {
    "Time": 5,
    "Temperature": 62.23,
    "Unit": "Celcius"
  },
  {
    "Time": 8,
    "Temperature": 85.12,
    "Unit": "Celcius"
  }
]

We need a class to deserialize the json file, we can try this:

public class TempSample
{
    public int Time { get; set; }
    public double Temperature { get; set; }
}

That is enough to read the data from the json file, but there are 2 things missing in that class, 1. It does not notifies the UI to update when a property changes, 2. LiveChars doesn't know how to draw this class. We need to implement INotifyPropertyChanged and IChartEntity to fix both issues.

To reduce the amount of boilerplate we will use the CommunityToolkit.Mvvm, it will help us to implement INotifyPropertyChanged:

public partial class TempSample : ObservableObject
{
    [ObservableProperty]
    private int _time;

    [ObservableProperty]
    private double _temperature;
}

We marked the class as partial and inherited from ObservableObject, finally we marked our fields with the ObservableProperty attribute, with these changes now the class implements INotifyPropertyChanged and also created the Time and Temperature properties, the class is ready to notify the UI to update when it changes.

Now to implement IChartEntity we need to add 2 properties the Coordinate (the location of the point in the UI) and the MetaData (just some information LiveCharts needs to build the chart).

public partial class TempSample : ObservableObject, IChartEntity
{
    [ObservableProperty]
    private int _time;

    [ObservableProperty]
    private double _temperature;

    public Coordinate Coordinate => new(Time, Temperature); // mark

    public ChartEntityMetaData? MetaData { get; set; } // mark
}

Now LiveCharts knows that we want the Time property in the X axis and the Temperature in the Y axis, we are ready to build charts with this class. But this example is creating a new instance of the Coordinate struct every time we access the property, we can cache the coordinate and only update the value of it when the Time or the Temperature properties change:

public partial class TempSample : ObservableObject, IChartEntity
{
    [ObservableProperty]
    private int _time;

    [ObservableProperty]
    private double _temperature;

    public Coordinate Coordinate { get; protected set; }  // mark

    public ChartEntityMetaData? MetaData { get; set; }

    protected override void OnPropertyChanged(PropertyChangedEventArgs e)  // mark
    {  // mark
        Coordinate = new(Time, Temperature);  // mark
        base.OnPropertyChanged(e);  // mark
    }  // mark
}

The change we made makes an important improvement in performance than the previous version, specially in large data sets, This is a general implementation that might work for most of the cases, it has a good performance, but there cases where you can simplify a lot in the implementation of IChartEntity, there are multiple ways to optimize it for performance, actually you can remove INotifyPropertyChanged interface it is not required by LiveCharts.

See full IChartEntity sample

Nulls and Coordinate.Empty

When LiveCharts finds a null instance or the coordinate is set to Coordinate.Empty it will skip the point:

image

Series = new ISeries[]
{
    new ColumnSeries<double?>
    {
        Values = new double?[] { 5, 4, null, 3, 2, 6, 5, 6, 2 }
    },

    new LineSeries<double?>
    {
        Values = new double?[] { 2, 6, 5, 3, null, 5, 2, 4, null }
    },

    new LineSeries<ObservablePoint?>
    {
        Values = new ObservablePoint?[]
        {
            new ObservablePoint { X = 0, Y = 1 },
            new ObservablePoint { X = 1, Y = 4 },
            null,
            new ObservablePoint { X = 4, Y = 5 },
            new ObservablePoint { X = 6, Y = 1 },
            new ObservablePoint { X = 8, Y = 6 },
        }
    }
};

You can also set the Coordinate to Coordinate.Empty inside a mapper, for example imagine a case where we need to skip a point when the Y property is null, in that case we could:

using LiveChartsCore.Kernel;

Series = new ISeries[]
{
    new LineSeries<City?>
    {
        Values = new City?[]
        {
            new City("London", 10),
            new City("Paris", 8),
            new City("Rome", null),
            new City("Berlin", 7),
        },
        Mapping = (city, chartPoint) =>
        {
            chartPoint.Coordinate =
                city.Population is null
                    ? Coordinate.Empty
                    : new Coordinate(chartPoint.Index, city.Population.Value);
        }
    }
};