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 function that takes both: the instance
(each TempSample
in our data collection) and the index
of the instance in the collection as parameters,
and returns a Coordinate
in the 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,
// use the Temperature property in the Y axis // mark
// and the Time property in the X axis // mark
Mapping = (sample, index) => new(sample.Time, sample.Temperature) // 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 starts
LiveCharts.Configure(config =>
config.HasMap<TempSample>(
(sample, index) => new(sample.Time, sample.Temperature));
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.
IChartEntity
The IChartEntity
interface forces 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 on 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.
Nulls and Coordinate.Empty
When LiveCharts finds a null
instance or the coordinate is set to Coordinate.Empty
it will skip the point:
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);
}
}
};