-
Notifications
You must be signed in to change notification settings - Fork 1
Home
Welcome to the yet-another-chart-component wiki!
From Package Manager Console:
PM> Install-Package eScapeLLC.UWP.Charts
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.
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.
The details of all the classes etc. used in YACC can be found at our documentation page in the API section.
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!
- XAML-oriented chart components.
- View Model friendly.
- Entire chart renders into a small number of
Canvas
usingPath
,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!
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.
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.
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.
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 secondColumnSeries
. - The
LineSeries
is between the secondColumnSeries
and theMarkerSeries
. - 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.
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.
And this is what we currently get from the above XAML and code:
©2017-22 eScape Technology LLC This project is Apache licensed!