Skip to content
g$ edited this page Feb 23, 2022 · 47 revisions

Welcome to the yet-another-chart-component wiki!

NuGet

From Package Manager Console:

   PM> Install-Package eScapeLLC.UWP.Charts

Package page on nuget.org

NuGet version

Windows Store

The demo application in the solution is available in Windows Store so you don't have to build it from source to see what it's like.

Freshness

This documentation wiki is tied to the current commit, and not the current package version! There will be periods where these are out-of-sync.

API Documentation

The details of all the classes etc. used in YACC can be found at our documentation page in the API section.

Cloners

Shout-out to everyone who has been cloning this repository! Any new features or enhancements would be gladly accepted via a Pull Request. Thanks in Advance!

Goals

  • XAML-oriented chart components.
  • View Model friendly.
  • Entire chart renders into a small number of Canvas using Path, Geometry etc.
  • Minimal reprocessing of series data.
  • Efficient resize behavior via transforms.
  • Independent data sources and series components.
  • "A la carte" visuals composition.
  • Rely on the XAML Style system for visual attributes.
  • Simple to use!

The Demo Chart

Let's get started! Here is a chart from the demo application (in the solution). If you've been following along, this chart has been getting ever-more "busy". This chart has tons going on:

  • a data source
  • four series depicting two values (two series for each value)
  • a horizontal band "bracketing" two computed values (average)
  • X and Y axes
  • value labels for both a series and the horizontal band
  • dynamic value labels on left-hand column series:
    • conditionally-generated labels for min and max values only
    • dynamically-reformatted text indicating the difference (and sign) between the two column values
    • conditionally-styled label based on the sign of the difference
  • using DataTemplates for custom value labels (band values)
  • other decorations
<yacc:Chart x:Name="chart" Style="{StaticResource Chart}" ChartError="Chart_ChartError"
	RelativePanel.Below="toolbar" RelativePanel.AlignBottomWithPanel="True"
	RelativePanel.AlignLeftWithPanel="True" RelativePanel.AlignRightWithPanel="True">
	<yacc:Chart.DataSources>
		<yacc:DataSource x:Name="data" Items="{Binding Data}"/>
	</yacc:Chart.DataSources>
	<yacc:Chart.Components>
		<yacc:Background PathStyle="{StaticResource Bkg}"/>
		<yacc:CategoryAxis x:Name="xaxis" Side="Bottom" DataSourceName="data" LabelPath="Label"
			PathStyle="{StaticResource Axes}"
			LabelStyle="{StaticResource CategoryAxisLabel2}" />
		<yacc:ColumnSeries x:Name="colv1" DataSourceName="data" ValuePath="Value1" ValueLabelPath="."
			ClipToDataRegion="True"
			Title="Value 1 Bar" ValueAxisName="yaxis" CategoryAxisName="xaxis"
			PathStyle="{StaticResource Column_v1}" BarOffset=".25" BarWidth=".25" />
		<yacc:ColumnSeries x:Name="colv2" DataSourceName="data" ValuePath="Value2"
			Title="Value 2 Bar" ValueAxisName="yaxis" CategoryAxisName="xaxis" ClipToDataRegion="True"
			PathStyle="{StaticResource Column_v2}" BarOffset=".5" BarWidth=".25" />
		<yacc:LineSeries x:Name="linev2" DataSourceName="data" ValuePath="Value2" Title="Value 2 Line"
			ValueAxisName="yaxis" CategoryAxisName="xaxis" CategoryAxisOffset=".375"
			ClipToDataRegion="False" PathStyle="{StaticResource Line_v2}" />
		<yacc:MarkerSeries DataSourceName="data" ValuePath="Value1" Title="Value 1 Marker"
			ValueAxisName="yaxis" CategoryAxisName="xaxis" MarkerOffset=".625" MarkerWidth=".25"
			PathStyle="{StaticResource Marker_v1}" MarkerTemplate="{StaticResource Marker}"/>
		<yacc:HorizontalBand x:Name="band" ValueAxisName="yaxis" Value1="{Binding Value1Average}" Value2="{Binding Value2Average}"
			DoMinMax="False" PathStyle="{StaticResource Band_v1-rule}" Value2PathStyle="{StaticResource Band_v2-rule}"
			BandPathStyle="{StaticResource Band_v1v2-band}"
			Visibility="{Binding ShowBand, Converter={StaticResource b2v}}" />
		<yacc:ValueAxis x:Name="yaxis" Side="Left" PathStyle="{StaticResource Axes}"
			LabelStyle="{StaticResource ValueAxisLabel}" LabelFormatString="F1" />
		<!-- CANNOT ElementName bind here we're tying into a different XAML namescope -->
		<yacc:ValueAxisGrid ValueAxisName="yaxis" PathStyle="{StaticResource Grid}"
			Visibility="{Binding ShowGrid, Converter={StaticResource b2v}}" />
		<yacc:ValueLabels SourceName="colv2" LabelFormatString="F2" CategoryAxisOffset=".625"
			PlacementOffset="0,1" LabelOffset="0,-1" LabelStyle="{StaticResource BigLabels}" />
		<yacc:ValueLabels SourceName="colv1" LabelFormatString="F2" CategoryAxisOffset=".375"
			PlacementOffset="0,1" LabelOffset="0,-1" LabelStyle="{StaticResource BigLabels}" />
		<yacc:ValueLabels SourceName="colv1" LabelFormatString="F2" CategoryAxisOffset=".375"
			PlacementOffset="0,1" LabelOffset="0,1" LabelStyle="{StaticResource BigLabels}"
			LabelFormatter="{StaticResource ColorLabel}" LabelSelector="{StaticResource MinMaxLabel}" />
		<!-- CANNOT ElementName bind here we're tying into a different XAML namescope -->
		<yacc:ValueLabels SourceName="band" LabelFormatString="F2" x:Name="values1"
			Visibility="{Binding ShowBand, Converter={StaticResource b2v}}"
			CategoryAxisOffset="0" PlacementOffset="0,1" LabelOffset="1,0" LabelTemplate="{StaticResource LabelWithBorder}" />
		<!-- CANNOT ElementName bind here we're tying into a different XAML namescope -->
		<yacc:ValueLabels SourceName="band" ValueChannel="1" LabelFormatString="F2" x:Name="values2"
			Visibility="{Binding ShowBand, Converter={StaticResource b2v}}"
			CategoryAxisOffset="1" PlacementOffset="0,0" LabelOffset="-1,0" LabelTemplate="{StaticResource LabelWithBorder2}" />
	</yacc:Chart.Components>
</yacc:Chart>

XAML is current as of latest commit. All the "outer" XAML is omitted for clarity. As you can tell by the attached properties, this Chart is contained in a RelativePanel. See Decorations for more info.

The Demo App is currently only compatible with 6.1.1 of Microsoft.Toolkit.Uwp.UI.Animations Nuget package, due to some breaking changes.

Overview

That seems quite verbose just to "make a chart", but it's a pretty big chart; the amount of flexibility provided makes up for it!

In YACC, everything is a "component", and you simply "stack" the components from back-to-front to build up the presentation.

The x:Name attribute is used to "name" things. This is the way to "connect" components to each other, e.g. series and axes, series and data sources. These properties end in Name.

Connecting to data source values is via the "member path" binding syntax. These properties end in Path.

Customizing visuals of "complex" elements, e.g. a series Path or a TextBlock axis label, use Style objects. These properties end in Style. Typically the component overrides certain properties that must be calculated, e.g. the width or height of a TextBlock; these cannot be styled.

Data

You likely have the data in one or more collections, ideally an IList or other IEnumerable. These go into the yacc:Chart.DataSources collection. Each yacc:DataSource element refers to a collection via data binding.

Rendering

The visual elements making up a chart go into the yacc:Chart.Components collection. A visual element is very loosely defined: background, axes, grid lines, and series are the ones currently available, and other "decorations" are coming.

Because the order of the components is the order of drawing, the demo Chart has these interesting features:

  • The value axis grid lines overlay all other elements.
  • The dreamy gradient background is behind all other elements.
  • The Value 1-2 Avg band overlays of all the series (but not the grid lines).
  • The MarkerSeries overlays the second ColumnSeries.
  • The LineSeries is between the second ColumnSeries and the MarkerSeries.
  • The different sets of ValueLabels overlay everything else.

Don't like that ordering? You have complete freedom to "play" with stacking order until the desired result is achieved.

View Model

Data access is straightforward; see below for some C#! There's also lots of code in the demo application. The DataSource is biased toward "instance" data, like the properties of a class (see below). This encourages collecting the chart's values into a single instance.

The (primitive) View Model classes from the demo application:

public class Observation {
	public String Label { get; private set; }
	public double Value1 { get; private set; }
	public double Value2 { get; private set; }
	public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now;
	public Observation(String label, double v1, double v2) { Label = label; Value1 = v1; Value2 = v2; }
}
public class ViewModel : INotifyPropertyChanged {
	readonly Random rnd = new Random();
	public event PropertyChangedEventHandler PropertyChanged;
	public int GroupCounter { get; set; }
	public double Value1Average { get; private set; }
	public double Value2Average { get; private set; }
	public ObservableCollection<Observation> Data { get; private set; }
	public ViewModel(IEnumerable<Observation> initial) {
		Data = new ObservableCollection<Observation>(initial);
		Recalculate();
	}
	void Changed(String name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }
	public void AddTail() {
		GroupCounter++;
		var obs = new Observation($"Group {GroupCounter}", 10*rnd.NextDouble() - 5, 10*rnd.NextDouble() - 4);
		Data.Add(obs);
		Recalculate();
	}
	public void AddHead() {
		GroupCounter++;
		var obs = new Observation($"Group {GroupCounter}", 10 * rnd.NextDouble() - 4, 10 * rnd.NextDouble() - 3);
		Data.Insert(0, obs);
		Recalculate();
	}
	public void RemoveHead() {
		if (Data.Count > 0) {
			Data.RemoveAt(0);
			Recalculate();
		}
	}
	public void RemoveTail() {
		if (Data.Count > 0) {
			Data.RemoveAt(Data.Count - 1);
			Recalculate();
		}
	}
	public void AddAndRemoveHead() {
		RemoveHead();
		AddTail();
		Recalculate();
	}
	void Recalculate() {
		if (Data.Count > 0) {
			Value1Average = Data.Average((ob) => ob.Value1);
			Value2Average = Data.Average((ob) => ob.Value2);
		}
		else {
			Value1Average = 0;
			Value2Average = 0;
		}
		Changed(nameof(Value1Average));
		Changed(nameof(Value2Average));
	}
}

This is yucky (I know)! Just for the sake of example. Comments removed for brevity.

The demo application now uses classes from our core package eScapeLLC.UWP.Core on nuget.org to make building it easier.

Page setup just assigns the initialized VM to Page.DataContext and our work here is done!

var vm = new ViewModel(new[] {
	new Observation("Group 1", -0.5, 0.02),
	new Observation("Group 2", 3, 10),
	new Observation("Group 3", 2, 5),
	new Observation("Group 4", 3, -10),
	new Observation("Group 5", 4, -5),
	new Observation("Group 6", -5.25, 0.04)
});
vm.GroupCounter = vm.Data.Count;
DataContext = vm;

Thanks Data Binding!

Notice we are not using a "scaled" X-axis, but rather a "category" X-axis, which is a series of discrete "cells" within which the series are positioned in a "relative" fashion; each "cell" internally ranges from [0..1] and is the unit for CategoryAxisOffset et al.

Screen Shot

And this is what we currently get from the above XAML and code: yacc demo screen shot

Clone this wiki locally