<!--
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
-->
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"
xmlns:charting="clr-namespace:Microsoft.Windows.Controls.DataVisualization.Charting;assembly=Microsoft.Windows.Controls.DataVisualization"
xmlns:samples="clr-namespace:Microsoft.Windows.Controls.Samples"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
x:Class="Microsoft.Windows.Controls.Samples.CustomSeriesSample">
<StackPanel>
<!-- Basic Examples -->
<ContentControl Content="Basic Examples" Style="{StaticResource Header}"/>
<controls:WrapPanel>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Constant">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110"
Title="f(x) = 110"
LineBrush="Blue"
LineThickness="2" />
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Linear">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110 + x"
Title="f(x) = 110 + x"
LineBrush="Green"
LineThickness="2" />
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Quadratic">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110 + (x - 4)^2"
LineBrush="Red"
LineThickness="1">
<samples:FunctionSeries.Title>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Row="1" Text="f(x) = 110 + (x - 4)" Margin="0 4 0 0" />
<TextBlock Grid.Column="1" Text="2" FontSize="8" Margin="0" />
</StackPanel>
</samples:FunctionSeries.Title>
</samples:FunctionSeries>
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Cubic">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110 + (x - 4)^3"
LineBrush="Black"
LineThickness="3">
<samples:FunctionSeries.Title>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Row="1" Text="f(x) = 110 + (x - 4)" Margin="0 4 0 0" />
<TextBlock Grid.Column="1" Text="3" FontSize="8" Margin="0" />
</StackPanel>
</samples:FunctionSeries.Title>
</samples:FunctionSeries>
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Inverse">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110 + 1 / (x - 4)"
LineThickness="1">
<samples:FunctionSeries.Title>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Row="1" Text="f(x) = 110 + " VerticalAlignment="Center" />
<StackPanel VerticalAlignment="Center">
<TextBlock Text="1" HorizontalAlignment="Center" />
<Line Stroke="Black" StrokeThickness="1" X1="0" X2="30" Y1="0" Y2="0" />
<TextBlock Text="x - 4" HorizontalAlignment="Center" />
</StackPanel>
</StackPanel>
</samples:FunctionSeries.Title>
</samples:FunctionSeries>
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart x:Name="CustomFunctionChart" Title="Custom Function">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Title="f(x) = 110 + 3 * Math.Sin(x)"
LineThickness="1" />
</charting:Chart.Series>
</charting:Chart>
</Grid>
</controls:WrapPanel>
<!-- Regression Scenario -->
<ContentControl Content="Regression Scenario" Style="{StaticResource Header}"/>
<Grid Height="500">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Right" Text="Regression: " />
<ComboBox
Grid.Column="1"
SelectionChanged="OnRegressionTypeChanged"
HorizontalAlignment="Left">
<ComboBox.ItemsSource>
<controls:ObjectCollection>
<sys:String>Linear</sys:String>
<sys:String>Quadratic</sys:String>
<sys:String>Cubic</sys:String>
<sys:String>Quartic</sys:String>
</controls:ObjectCollection>
</ComboBox.ItemsSource>
</ComboBox>
<charting:Chart x:Name="ParticulateAnalysis" Grid.Row="1" Grid.ColumnSpan="2" Title="Particulate Level Analysis">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Title="Regression"
LineBrush="Blue"
LineThickness="2" />
</charting:Chart.Series>
</charting:Chart>
</Grid>
<src:SourceViewer xmlns:src="clr-namespace:Microsoft.Windows.Controls.Samples;assembly=Microsoft.Windows.Controls.Samples.Common" xmlns:sys="clr-namespace:System;assembly=mscorlib">
<src:SourceFile Path="CustomSeriesSample.xaml">
<src:SourceFile.Source>
<sys:String><!--
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
-->
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"
xmlns:charting="clr-namespace:Microsoft.Windows.Controls.DataVisualization.Charting;assembly=Microsoft.Windows.Controls.DataVisualization"
xmlns:samples="clr-namespace:Microsoft.Windows.Controls.Samples"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
x:Class="Microsoft.Windows.Controls.Samples.CustomSeriesSample">
<StackPanel>
<!-- Basic Examples -->
<ContentControl Content="Basic Examples" Style="{StaticResource Header}"/>
<controls:WrapPanel>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Constant">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110"
Title="f(x) = 110"
LineBrush="Blue"
LineThickness="2" />
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Linear">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110 + x"
Title="f(x) = 110 + x"
LineBrush="Green"
LineThickness="2" />
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Quadratic">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110 + (x - 4)^2"
LineBrush="Red"
LineThickness="1">
<samples:FunctionSeries.Title>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Row="1" Text="f(x) = 110 + (x - 4)" Margin="0 4 0 0" />
<TextBlock Grid.Column="1" Text="2" FontSize="8" Margin="0" />
</StackPanel>
</samples:FunctionSeries.Title>
</samples:FunctionSeries>
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Cubic">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110 + (x - 4)^3"
LineBrush="Black"
LineThickness="3">
<samples:FunctionSeries.Title>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Row="1" Text="f(x) = 110 + (x - 4)" Margin="0 4 0 0" />
<TextBlock Grid.Column="1" Text="3" FontSize="8" Margin="0" />
</StackPanel>
</samples:FunctionSeries.Title>
</samples:FunctionSeries>
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart Title="Inverse">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Function="110 + 1 / (x - 4)"
LineThickness="1">
<samples:FunctionSeries.Title>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Row="1" Text="f(x) = 110 + " VerticalAlignment="Center" />
<StackPanel VerticalAlignment="Center">
<TextBlock Text="1" HorizontalAlignment="Center" />
<Line Stroke="Black" StrokeThickness="1" X1="0" X2="30" Y1="0" Y2="0" />
<TextBlock Text="x - 4" HorizontalAlignment="Center" />
</StackPanel>
</StackPanel>
</samples:FunctionSeries.Title>
</samples:FunctionSeries>
</charting:Chart.Series>
</charting:Chart>
</Grid>
<Grid Style="{StaticResource WrapperStyle}">
<charting:Chart x:Name="CustomFunctionChart" Title="Custom Function">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Title="f(x) = 110 + 3 * Math.Sin(x)"
LineThickness="1" />
</charting:Chart.Series>
</charting:Chart>
</Grid>
</controls:WrapPanel>
<!-- Regression Scenario -->
<ContentControl Content="Regression Scenario" Style="{StaticResource Header}"/>
<Grid Height="500">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Right" Text="Regression: " />
<ComboBox
Grid.Column="1"
SelectionChanged="OnRegressionTypeChanged"
HorizontalAlignment="Left">
<ComboBox.ItemsSource>
<controls:ObjectCollection>
<sys:String>Linear</sys:String>
<sys:String>Quadratic</sys:String>
<sys:String>Cubic</sys:String>
<sys:String>Quartic</sys:String>
</controls:ObjectCollection>
</ComboBox.ItemsSource>
</ComboBox>
<charting:Chart x:Name="ParticulateAnalysis" Grid.Row="1" Grid.ColumnSpan="2" Title="Particulate Level Analysis">
<charting:Chart.Series>
<charting:ScatterSeries
Title="Particulate Levels"
ItemsSource="{Binding LevelsInRainfall, Source={StaticResource ParticulateLevel}}"
IndependentValueBinding="{Binding Rainfall}"
DependentValueBinding="{Binding Particulate}" />
<samples:FunctionSeries
Title="Regression"
LineBrush="Blue"
LineThickness="2" />
</charting:Chart.Series>
</charting:Chart>
</Grid>
</StackPanel>
</UserControl></sys:String>
</src:SourceFile.Source>
</src:SourceFile>
<src:SourceFile Path="CustomSeriesSample.xaml.cs">
<src:SourceFile.Source>
<sys:String>// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.Windows.Controls.DataVisualization.Charting;
using System.ComponentModel;
namespace Microsoft.Windows.Controls.Samples
{
/// <summary>
/// Charting sample demonstrating how to create a custom series.
/// </summary>
[Sample("(2)Custom Series", DifficultyLevel.Advanced)]
[Category("DataVisualization")]
public partial class CustomSeriesSample : UserControl
{
/// <summary>
/// Initializes a new instance of the CustomSeriesSample class.
/// </summary>
public CustomSeriesSample()
{
InitializeComponent();
// Use a custom function for a series
FunctionSeries series = CustomFunctionChart.Series[1] as FunctionSeries;
series.Function = x => 110 + 3 * Math.Sin(x);
}
/// <summary>
/// Perform a regression against the particulate levels data.
/// </summary>
/// <param name="sender">The regression ComboBox.</param>
/// <param name="e">Event arguments.</param>
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by an event handler in XAML.")]
[SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Body", Justification = "Simplifies the sample.")]
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Simplifies the sample.")]
private void OnRegressionTypeChanged(object sender, SelectionChangedEventArgs e)
{
// Get the options and the series
ComboBox combo = sender as ComboBox;
if (combo == null || ParticulateAnalysis == null)
{
return;
}
ScatterSeries dataSeries = ParticulateAnalysis.Series[0] as ScatterSeries;
FunctionSeries regressionSeries = ParticulateAnalysis.Series[1] as FunctionSeries;
if (dataSeries == null || regressionSeries == null)
{
return;
}
// Get the active DataPoints (this assumes the default template for
// ScatterSeries)
Canvas plotArea = VisualTreeHelper.GetChild(dataSeries, 0) as Canvas;
if (plotArea == null)
{
return;
}
List<DataPoint> activePoints =
plotArea
.Children
.OfType<DataPoint>()
.ToList();
// The dimensions were added linearly to the ComboBox
int dimension = combo.SelectedIndex + 1;
// Initialize a simple least squares analysis
int i = 0;
int j = 0;
int k = 0;
double[] y = new double[activePoints.Count];
double[,] x = new double[activePoints.Count, dimension + 1];
for (i = 0; i < activePoints.Count; i++)
{
DataPoint point = activePoints[i];
double independentValue = Convert.ToDouble(point.IndependentValue, CultureInfo.InvariantCulture);
for (j = 0; j <= dimension; j++)
{
x[i, j] = Math.Pow(independentValue, j);
}
y[i] = Convert.ToDouble(point.DependentValue, CultureInfo.InvariantCulture);
}
// Create the equations
double[][] matrix = new double[dimension + 1][];
for (i = 0; i <= dimension; i++)
{
// Create the row
matrix[i] = new double[dimension + 2];
// indeterminate coefficients
for (j = 0; j <= dimension; j++)
{
matrix[i][j] = 0.0;
for (k = 0; k < activePoints.Count; k++)
{
matrix[i][j] += x[k, i] * x[k, j];
}
}
// determinate values
for (k = 0; k < activePoints.Count; k++)
{
matrix[i][dimension + 1] += x[k, i] * y[k];
}
}
// Convert to row-echelon form
i = 0;
j = 0;
while (i <= dimension && j <= dimension)
{
// Get the pivot in column j starting at row i
int pivotRow = i;
for (k = i; k <= dimension; k++)
{
if (Math.Abs(matrix[k][j]) > Math.Abs(matrix[pivotRow][j]))
{
pivotRow = k;
}
}
double pivot = matrix[pivotRow][j];
// If we have a pivot element
if (pivot != 0)
{
// Swap the current row with the pivot row
double[] temp = matrix[i];
matrix[i] = matrix[pivotRow];
matrix[pivotRow] = temp;
pivotRow = i;
// Normalize the pivot row to the pivot
double c = matrix[i][j];
for (k = 0; k <= dimension + 1; k++)
{
matrix[i][k] /= c;
}
// Clear out the pivot position from the remaining rows
for (k = i + 1; k <= dimension; k++)
{
c = matrix[k][j];
for (int m = i; m <= dimension + 1; m++)
{
matrix[k][m] -= c * matrix[i][m];
}
}
i++;
}
j++;
}
// Solve using substitution
for (i = dimension - 1; i >= 0; i--)
{
for (j = dimension; j > i; j--)
{
matrix[i][dimension + 1] -= matrix[i][j] * matrix[j][dimension + 1];
matrix[i][j] = 0;
}
}
// Capture the coefficients
double a0 = matrix[0][dimension + 1];
double a1 = matrix[1][dimension + 1];
double a2 = (dimension >= 2) ? matrix[2][dimension + 1] : double.NaN;
double a3 = (dimension >= 3) ? matrix[3][dimension + 1] : double.NaN;
double a4 = (dimension == 4) ? matrix[4][dimension + 1] : double.NaN;
// Create the function
Func<double, double> function = null;
switch (dimension)
{
case 1:
function = z => a1 * z + a0;
break;
case 2:
function = z => a2 * z * z + a1 * z + a0;
break;
case 3:
function = z => a3 * z * z * z + a2 * z * z + a1 * z + a0;
break;
case 4:
function = z => a4 * z * z * z * z + a3 * z * z * z + a2 * z * z + a1 * z + a0;
break;
}
// Create the title
StackPanel title = new StackPanel { Orientation = Orientation.Horizontal };
title.Children.Add(
new TextBlock
{
Text = "f(x) = ",
Margin = new Thickness(0, 4, 0, 0)
});
title.Children.Add(
new TextBlock
{
Text = a0.ToString("N3", CultureInfo.InvariantCulture),
Margin = new Thickness(0, 4, 0, 0)
});
AddTitleTerm(title, a1, 1);
if (dimension >= 2)
{
AddTitleTerm(title, a2, 2);
}
if (dimension >= 3)
{
AddTitleTerm(title, a3, 3);
}
if (dimension == 4)
{
AddTitleTerm(title, a4, 4);
}
// Set the function and the title
regressionSeries.Function = function;
regressionSeries.Title = title;
}
/// <summary>
/// Add a term to the title.
/// </summary>
/// <param name="title">The title container.</param>
/// <param name="value">The value of the term.</param>
/// <param name="exponent">The exponent of the term.</param>
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by a method called by an event handler in XAML.")]
private static void AddTitleTerm(StackPanel title, double value, int exponent)
{
if (value == 0)
{
return;
}
title.Children.Add(
new TextBlock
{
Text = value >= 0 ? " + " : " - ",
Margin = new Thickness(0, 4, 0, 0)
});
title.Children.Add(
new TextBlock
{
Text = string.Format(CultureInfo.InvariantCulture, "{0:N3}x", Math.Abs(value)),
Margin = new Thickness(0, 4, 0, 0)
});
if (exponent > 1)
{
title.Children.Add(
new TextBlock
{
Text = exponent.ToString(CultureInfo.InvariantCulture),
FontSize = 8
});
}
}
}
}</sys:String>
</src:SourceFile.Source>
</src:SourceFile>
<src:SourceFile Path="FunctionSeries.cs">
<src:SourceFile.Source>
<sys:String>// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Media;
using Microsoft.Windows.Controls.DataVisualization;
using Microsoft.Windows.Controls.DataVisualization.Charting;
namespace Microsoft.Windows.Controls.Samples
{
/// <summary>
/// FunctionSeries is used to single variable functions on a chart.
/// </summary>
[TemplatePart(Name = FunctionSeries.PlotAreaName, Type = typeof(Canvas))]
public sealed partial class FunctionSeries : Series, IRangeAxisInformationProvider
{
/// <summary>
/// The default control template would normally reside in generic.xaml,
/// but the sample project is an application and doesn't have that file.
/// We're just putting it here, but a real control project wouldn't.
/// </summary>
private const string DefaultTemplate =
@"<ControlTemplate
xmlns='http://schemas.microsoft.com/client/2007'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:samples='clr-namespace:Microsoft.Windows.Controls.Samples;assembly=Microsoft.Windows.Controls.Samples'
TargetType='samples:FunctionSeries'>
<Canvas x:Name='PlotArea'>
<Path
Stroke='{TemplateBinding LineBrush}'
StrokeThickness='{TemplateBinding LineThickness}'
Data='{TemplateBinding Geometry}' />
</Canvas>
</ControlTemplate>";
#region Template Parts
/// <summary>
/// Name of the plot area canvas.
/// </summary>
private const string PlotAreaName = "PlotArea";
/// <summary>
/// Gets or sets the plot area canvas.
/// </summary>
private Canvas PlotArea { get; set; }
#endregion Template Parts
#region public Func<double, double> Function
/// <summary>
/// Gets or sets the function to plot.
/// </summary>
[TypeConverter(typeof(SimpleFunctionTypeConverter))]
public Func<double, double> Function
{
get { return GetValue(FunctionProperty) as Func<double, double>; }
set { SetValue(FunctionProperty, value); }
}
/// <summary>
/// Identifies the Function dependency property.
/// </summary>
public static readonly DependencyProperty FunctionProperty =
DependencyProperty.Register(
"Function",
typeof(Func<double, double>),
typeof(FunctionSeries),
new PropertyMetadata(null, OnFunctionPropertyChanged));
/// <summary>
/// FunctionProperty property changed handler.
/// </summary>
/// <param name="d">FunctionSeries that changed its Function.</param>
/// <param name="e">Event arguments.</param>
private static void OnFunctionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FunctionSeries source = d as FunctionSeries;
source.Refresh();
}
#endregion public Func<double, double> Function
#region public Geometry Geometry
/// <summary>
/// Gets or sets the geometry of the line object rendering the function.
/// </summary>
public Geometry Geometry
{
get { return GetValue(GeometryProperty) as Geometry; }
set { SetValue(GeometryProperty, value); }
}
/// <summary>
/// Identifies the Geometry dependency property.
/// </summary>
public static readonly DependencyProperty GeometryProperty =
DependencyProperty.Register(
"Geometry",
typeof(Geometry),
typeof(FunctionSeries),
new PropertyMetadata(null));
#endregion public Geometry Geometry
#region public Brush LineBrush
/// <summary>
/// Gets or sets the brush used to plot the function.
/// </summary>
public Brush LineBrush
{
get { return GetValue(LineBrushProperty) as Brush; }
set { SetValue(LineBrushProperty, value); }
}
/// <summary>
/// Identifies the LineBrush dependency property.
/// </summary>
public static readonly DependencyProperty LineBrushProperty =
DependencyProperty.Register(
"LineBrush",
typeof(Brush),
typeof(FunctionSeries),
new PropertyMetadata(null, OnLineBrushPropertyChanged));
/// <summary>
/// LineBrushProperty property changed handler.
/// </summary>
/// <param name="d">FunctionSeries that changed its LineBrush.</param>
/// <param name="e">Event arguments.</param>
private static void OnLineBrushPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FunctionSeries source = d as FunctionSeries;
Brush value = e.NewValue as Brush;
source.LegendItem.DataContext = new ContentControl { Background = value };
}
#endregion public Brush LineBrush
#region public double LineThickness
/// <summary>
/// Gets or sets the thickness of the line used to plot the function.
/// </summary>
public double LineThickness
{
get { return (double) GetValue(LineThicknessProperty); }
set { SetValue(LineThicknessProperty, value); }
}
/// <summary>
/// Identifies the LineThickness dependency property.
/// </summary>
public static readonly DependencyProperty LineThicknessProperty =
DependencyProperty.Register(
"LineThickness",
typeof(double),
typeof(FunctionSeries),
new PropertyMetadata(1.0));
#endregion public double LineThickness
#region private IRangeAxis IndependentAxis
/// <summary>
/// Gets or sets the value of the independent axis.
/// </summary>
private IRangeAxis IndependentAxis
{
get { return GetValue(IndependentAxisProperty) as IRangeAxis; }
set { SetValue(IndependentAxisProperty, value); }
}
/// <summary>
/// Identifies the IndependentAxis dependency property.
/// </summary>
private static readonly DependencyProperty IndependentAxisProperty =
DependencyProperty.Register(
"IndependentAxis",
typeof(IRangeAxis),
typeof(FunctionSeries),
null);
#endregion protected IRangeAxis IndependentAxis
#region private IRangeAxis DependentAxis
/// <summary>
/// Gets or sets the value of the dependent axis.
/// </summary>
private IRangeAxis DependentAxis
{
get { return GetValue(DependentAxisProperty) as IRangeAxis; }
set { SetValue(DependentAxisProperty, value); }
}
/// <summary>
/// Identifies the DependentAxis dependency property.
/// </summary>
private static readonly DependencyProperty DependentAxisProperty =
DependencyProperty.Register(
"DependentAxis",
typeof(IRangeAxis),
typeof(FunctionSeries),
null);
#endregion protected IRangeAxis DependentAxis
/// <summary>
/// Gets or sets the single chart legend item associated with the
/// series.
/// </summary>
private LegendItem LegendItem { get; set; }
/// <summary>
/// Gets or sets the Geometry used to clip the line to the PlotArea
/// bounds.
/// </summary>
private RectangleGeometry ClipGeometry { get; set; }
/// <summary>
/// Initializes a new instance of the FunctionSeries class.
/// </summary>
public FunctionSeries()
{
LegendItem = new LegendItem();
LegendItems.Add(LegendItem);
Clip = ClipGeometry = new RectangleGeometry();
SizeChanged += OnSizeChanged;
// Explicitly load the default template since the samples project
// is an application and does not have a generic.xaml file.
Template = XamlReader.Load(DefaultTemplate) as ControlTemplate;
LineBrush = new SolidColorBrush(Colors.Purple);
}
/// <summary>
/// Refreshes data from data source and renders the series.
/// </summary>
public override void Refresh()
{
if (SeriesHost == null || ActualWidth == 0)
{
return;
}
// Ensure we have a function to plot
Func<double, double> function = Function;
if (function == null)
{
return;
}
// Ensure we have axes
IRangeAxis independent = GetAxis(AxisOrientation.Horizontal, IndependentAxis);
IndependentAxis = independent;
IRangeAxis dependent = GetAxis(AxisOrientation.Vertical, DependentAxis);
DependentAxis = dependent;
if (!independent.Range.HasData)
{
return;
}
// Create a geometry that matches the function to plot
PathGeometry path = new PathGeometry();
PathFigure figure = new PathFigure();
// Get the range over which we will
double start = (double) independent.Range.Minimum;
double end = (double) independent.Range.Maximum;
// Adjust the line at each pixel
double delta = (end - start) / ActualWidth;
// We'll only add a new line segment when the slope is changing
// between points
Point last = GetPoint(start, function, independent, dependent);
figure.StartPoint = last;
double slope = double.NaN;
for (double x = start + delta; x <= end; x += delta)
{
Point next = GetPoint(x, function, independent, dependent);
double newSlope = (next.Y - last.Y) / (next.X - last.X);
if (slope != newSlope)
{
figure.Segments.Add(new LineSegment { Point = last });
}
slope = newSlope;
last = next;
}
figure.Segments.Add(new LineSegment { Point = last });
path.Figures.Add(figure);
Geometry = path;
}
/// <summary>
/// Get a point in screen coordinates.
/// </summary>
/// <param name="x">Independent value.</param>
/// <param name="function">The function.</param>
/// <param name="independent">The independent axis.</param>
/// <param name="dependent">The dependent axis.</param>
/// <returns>The point in screen coordinates.</returns>
private Point GetPoint(double x, Func<double, double> function, IRangeAxis independent, IRangeAxis dependent)
{
// Get the dependent value
double y = double.NaN;
try
{
y = function(x);
}
catch (DivideByZeroException)
{
}
// Map the actual values into coordinate values
return new Point(
independent.GetPlotAreaCoordinate(x),
Math.Min(
Math.Max(
ActualHeight - dependent.GetPlotAreaCoordinate(y),
-1),
ActualHeight + 1));
}
/// <summary>
/// Get the plot area after loading it from XAML.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PlotArea = GetTemplateChild("PlotArea") as Canvas;
}
/// <summary>
/// Updates the visual appearance of all the data points when the size
/// changes.
/// </summary>
/// <param name="sender">The series.</param>
/// <param name="e">Event arguments.</param>
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
// Update the clip geometry
ClipGeometry.Rect = new Rect(0.0, 0.0, e.NewSize.Width, e.NewSize.Height);
// Update the PlotArea size and refresh.
if (PlotArea != null)
{
PlotArea.Width = e.NewSize.Width;
PlotArea.Height = e.NewSize.Height;
Refresh();
}
}
/// <summary>
/// Sets all the text the legend items to the title.
/// </summary>
/// <param name="oldValue">The old title.</param>
/// <param name="newValue">The new title.</param>
protected override void OnTitleChanged(object oldValue, object newValue)
{
base.OnTitleChanged(oldValue, newValue);
LegendItem.Content = Title;
}
/// <summary>
/// Get or create a linear numeric axis in the correct dimension.
/// </summary>
/// <param name="orientation">Dimension of the axis to create.</param>
/// <param name="oldAxis">
/// Old value of the axis in this dimension.
/// </param>
/// <returns>New value of the axis in this dimension.</returns>
private IRangeAxis GetAxis(AxisOrientation orientation, IRangeAxis oldAxis)
{
// Check the existing axes for a potential axis
IRangeAxis axis =
(from IRangeAxis a in SeriesHost.Axes.OfType<IRangeAxis>()
where a.Orientation == orientation
select a)
.FirstOrDefault();
if (axis == null)
{
// Create a new axis if not found
axis = new LinearAxis
{
Orientation = orientation,
};
}
if (oldAxis != axis)
{
// Unregister any existing axis
if (oldAxis != null)
{
SeriesHost.UnregisterWithAxis(this, oldAxis);
oldAxis.Invalidated -= OnAxisInvalidated;
}
// Register the new axis
SeriesHost.RegisterWithAxis(this, axis);
axis.Invalidated += OnAxisInvalidated;
}
return axis;
}
/// <summary>
/// Updates the series when the axis is invalidated.
/// </summary>
/// <param name="sender">The axis that was invalidated.</param>
/// <param name="e">Event arguments.</param>
private void OnAxisInvalidated(object sender, EventArgs e)
{
if (DependentAxis != null && IndependentAxis != null)
{
Refresh();
}
}
/// <summary>
/// Ensures that chart and series are kept in a consistent state when a
/// series is added or removed from a chart.
/// </summary>
/// <param name="oldValue">Old chart.</param>
/// <param name="newValue">New chart.</param>
protected override void OnSeriesHostPropertyChanged(ISeriesHost oldValue, ISeriesHost newValue)
{
IRangeAxis axis = null;
// Unregister the axes from the old chart
if (oldValue != null)
{
axis = IndependentAxis;
if (axis != null)
{
oldValue.UnregisterWithAxis(this, axis);
IndependentAxis = null;
}
axis = DependentAxis;
if (axis != null)
{
oldValue.UnregisterWithAxis(this, axis);
DependentAxis = null;
}
}
// Register the axes with new chart
if (newValue != null)
{
axis = IndependentAxis;
if (axis != null)
{
newValue.RegisterWithAxis(this, axis);
}
axis = DependentAxis;
if (axis != null)
{
newValue.RegisterWithAxis(this, axis);
}
}
base.OnSeriesHostPropertyChanged(oldValue, newValue);
}
/// <summary>
/// If data is found returns the minimum and maximum dependent numeric
/// values.
/// </summary>
/// <param name="rangeAxis">IRangeAxis that needs the data.</param>
/// <returns>
/// The range of values or empty if no data is present.
/// </returns>
public Range<IComparable> GetActualRange(IRangeAxis rangeAxis)
{
// Use an empty range so we only plot over the area used by other
// axes.
return new Range<IComparable>();
}
/// <summary>
/// If data is found returns the minimum and maximum dependent numeric
/// values.
/// </summary>
/// <param name="rangeAxis">IRangeAxis that needs the data.</param>
/// <returns>
/// The range of values or empty if no data is present.
/// </returns>
public Range<IComparable> GetDesiredRange(IRangeAxis rangeAxis)
{
// Use an empty range so we only plot over the area used by other
// axes.
return new Range<IComparable>();
}
}
}</sys:String>
</src:SourceFile.Source>
</src:SourceFile>
<src:SourceFile Path="SimpleFunctionTypeConverter.cs">
<src:SourceFile.Source>
<sys:String>// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq.Expressions;
using System.Text;
namespace Microsoft.Windows.Controls.Samples
{
/// <summary>
/// TypeConverter used for creating single variable functions from simple
/// arithmetic expressions.
/// </summary>
public class SimpleFunctionTypeConverter : TypeConverter
{
/// <summary>
/// Initializes a new instance of the SimpleFunctionTypeConverter class.
/// </summary>
public SimpleFunctionTypeConverter()
{
}
/// <summary>
/// Determine whether the sourceType can be converted to a single
/// variable function.
/// </summary>
/// <param name="context">Conversion context.</param>
/// <param name="sourceType">The type to convert from.</param>
/// <returns>
/// A value indicating whether the type can be converted.
/// </returns>
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
/// <summary>
/// Convert the value into a single variable function.
/// </summary>
/// <param name="context">Conversion context.</param>
/// <param name="culture">Conversion culture.</param>
/// <param name="value">The value to convert.</param>
/// <returns>A single variable function.</returns>
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
Func<double, double> function = null;
string text = value as string;
if (!string.IsNullOrEmpty(text))
{
function = new Parser(text).Function;
}
return function;
}
/// <summary>
/// Defines the type of a token.
/// </summary>
private enum TokenType
{
/// <summary>
/// A numeric value..
/// </summary>
Number,
/// <summary>
/// An identifier.
/// </summary>
Identifier,
/// <summary>
/// The addition operator.
/// </summary>
Addition,
/// <summary>
/// The substraction operator.
/// </summary>
Subtraction,
/// <summary>
/// The exponentiation operator.
/// </summary>
Multiplication,
/// <summary>
/// The division operator.
/// </summary>
Division,
/// <summary>
/// The exponentiation operator.
/// </summary>
Exponentiation,
/// <summary>
/// A left parenthesis.
/// </summary>
LeftParenthesis,
/// <summary>
/// A right parenthesis.
/// </summary>
RightParenthesis
}
/// <summary>
/// Represents a lexical token.
/// </summary>
private class Token
{
/// <summary>
/// Gets or sets the type of the token.
/// </summary>
public TokenType TokenType { get; set; }
/// <summary>
/// Gets or sets the value of a token (for numbers and identifiers).
/// </summary>
public object Value { get; set; }
/// <summary>
/// Initializes a new instance of the Token class.
/// </summary>
public Token()
{
}
}
/// <summary>
/// Perform lexical analysis of simple expressions.
/// </summary>
private class Lexer
{
/// <summary>
/// Gets or sets the input string to scan.
/// </summary>
internal string Input { get; set; }
/// <summary>
/// Gets or sets the current position of the lexer.
/// </summary>
internal int Position { get; set; }
/// <summary>
/// Gets a value indicating whether the lexer has room to advance
/// through the input.
/// </summary>
internal bool CanAdvance
{
get { return Position < Input.Length; }
}
/// <summary>
/// Gets the character at the current input position.
/// </summary>
private char Current
{
get { return Input[Position]; }
}
/// <summary>
/// Gets or sets the lookahead token.
/// </summary>
private Token Lookahead { get; set; }
/// <summary>
/// Initializes a new instance of the Lexer class.
/// </summary>
/// <param name="input">The input to analyze.</param>
public Lexer(string input)
{
Debug.Assert(!string.IsNullOrEmpty(input), "input shuould not be null or empty!");
Input = input;
}
/// <summary>
/// Advance the token to the next input.
/// </summary>
/// <returns>The token that was read.</returns>
private Token ReadNext()
{
// Eat as much whitespace as possible
while (CanAdvance && char.IsWhiteSpace(Current))
{
Position++;
}
// Match a literal token
Token token =
MatchLiteral('(', TokenType.LeftParenthesis) ??
MatchLiteral(')', TokenType.RightParenthesis) ??
MatchLiteral('+', TokenType.Addition) ??
MatchLiteral('-', TokenType.Subtraction) ??
MatchLiteral('*', TokenType.Multiplication) ??
MatchLiteral('/', TokenType.Division) ??
MatchLiteral('^', TokenType.Exponentiation);
// Match identifier or number tokens
if (token == null)
{
int start = Position;
// Try and match identifiers
while (CanAdvance && char.IsLetter(Current))
{
Position++;
}
if (start != Position)
{
token = new Token { TokenType = TokenType.Identifier };
token.Value = Input.Substring(start, Position - start);
}
else
{
// Try and match numbers
while (CanAdvance && char.IsDigit(Current))
{
Position++;
}
if (CanAdvance && Current == '.')
{
Position++;
}
while (CanAdvance && char.IsDigit(Current))
{
Position++;
}
if (start != Position)
{
token = new Token { TokenType = TokenType.Number };
token.Value = double.Parse(Input.Substring(start, Position - start), CultureInfo.InvariantCulture);
}
}
}
if (token != null)
{
return token;
}
else if (CanAdvance)
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unknown token at position {0}!", Position));
}
else
{
return null;
}
}
/// <summary>
/// Match a literal token.
/// </summary>
/// <param name="tokenChar">Character of the token.</param>
/// <param name="tokenType">The type of the token.</param>
/// <returns>The literal token, if matched.</returns>
private Token MatchLiteral(char tokenChar, TokenType tokenType)
{
if (CanAdvance && Current == tokenChar)
{
Position++;
return new Token { TokenType = tokenType };
}
return null;
}
/// <summary>
/// Get the next input token.
/// </summary>
/// <returns>The next input token.</returns>
public Token Get()
{
Token token = null;
if (Lookahead != null)
{
token = Lookahead;
Lookahead = null;
}
else
{
token = ReadNext();
}
return token;
}
/// <summary>
/// Peek at the lookahead token.
/// </summary>
/// <returns>The lookahead token.</returns>
public Token Peek()
{
if (Lookahead == null)
{
Lookahead = ReadNext();
}
return Lookahead;
}
}
/// <summary>
/// Perform syntactic analysis of simple expressions.
/// </summary>
/// <remarks>
/// The parser uses the following grammar:
/// {Expression}
/// := {Term} '+' {Expression}
/// | {Term} '-' {Expression}
/// | {Term}
/// {Term}
/// := {Exponent} '*' {Term}
/// | {Exponent} '/' {Term}
/// | {Exponent}
/// {Exponent}
/// := {Factor} '^' {Exponent}
/// | {Factor}
/// {Factor}
/// := {Number}
/// | {Identifier}
/// | '(' {Expression} ')'
/// </remarks>
private class Parser
{
/// <summary>
/// Gets or sets the lexer used for lexical analysis.
/// </summary>
private Lexer Lexer { get; set; }
/// <summary>
/// Gets or sets the single variable of the function.
/// </summary>
private ParameterExpression Parameter { get; set; }
/// <summary>
/// Gets the function created from the input.
/// </summary>
public Func<double, double> Function { get; private set; }
/// <summary>
/// Initializes a new instance of the Parser class.
/// </summary>
/// <param name="input">The input to analyze.</param>
public Parser(string input)
{
Lexer = new Lexer(input);
Parse();
}
/// <summary>
/// Parse the input and create a function.
/// </summary>
private void Parse()
{
// Build the expression
Expression expression = GetExpression();
// Ensure we exhausted the input
int finalPosition = Lexer.Position;
Token finalToken = Lexer.Get();
if (finalToken != null)
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unexpected token {0} at position {1}!", finalToken.TokenType, finalPosition));
}
// Wrap the expression in a function
Expression<Func<double, double>> functionExpression =
Expression.Lambda<Func<double, double>>(
expression,
Parameter ?? Expression.Parameter(typeof(double), "x"));
// Compile the expression into a function
Function = functionExpression.Compile();
}
/// <summary>
/// Read an expression.
/// </summary>
/// <returns>The parsed expression.</returns>
private Expression GetExpression()
{
Expression term = GetTerm();
if (TryMatch(TokenType.Addition))
{
Expression expr = GetExpression();
return Expression.Add(term, expr);
}
else if (TryMatch(TokenType.Subtraction))
{
Expression expr = GetExpression();
return Expression.Subtract(term, expr);
}
return term;
}
/// <summary>
/// Read a term.
/// </summary>
/// <returns>The parsed term.</returns>
private Expression GetTerm()
{
Expression exponent = GetExponent();
if (TryMatch(TokenType.Multiplication))
{
Expression term = GetTerm();
return Expression.Multiply(exponent, term);
}
else if (TryMatch(TokenType.Division))
{
Expression term = GetTerm();
return Expression.Divide(exponent, term);
}
return exponent;
}
/// <summary>
/// Read an exponent.
/// </summary>
/// <returns>The parsed exponent.</returns>
private Expression GetExponent()
{
Expression factor = GetFactor();
if (TryMatch(TokenType.Exponentiation))
{
Expression power = GetExponent();
return Expression.Power(factor, power);
}
else
{
return factor;
}
}
/// <summary>
/// Read a factor.
/// </summary>
/// <returns>The parsed factor.</returns>
private Expression GetFactor()
{
Token token = Lexer.Get();
if (token != null)
{
if (token.TokenType == TokenType.Number)
{
return Expression.Constant(token.Value, typeof(double));
}
else if (token.TokenType == TokenType.Identifier)
{
string name = token.Value as string;
// Linq expressions use referential equality on
// parameters, so we need to use the same instance
if (Parameter != null && Parameter.Name != name)
{
throw new FormatException("Only single variable functions are supported!");
}
else if (Parameter == null)
{
Parameter = Expression.Parameter(typeof(double), name);
}
return Parameter;
}
else if (token.TokenType == TokenType.LeftParenthesis)
{
Expression nested = GetExpression();
if (TryMatch(TokenType.RightParenthesis))
{
return nested;
}
}
}
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unexpected token type at position {0}!", Lexer.Position));
}
/// <summary>
/// Try to match a token.
/// </summary>
/// <param name="tokenType">The type of token to match.</param>
/// <returns>
/// A value indicating whether the token was matched.
/// </returns>
private bool TryMatch(TokenType tokenType)
{
Token token = Lexer.Peek();
if (token != null && token.TokenType == tokenType)
{
Lexer.Get();
return true;
}
return false;
}
}
}
}</sys:String>
</src:SourceFile.Source>
</src:SourceFile>
</src:SourceViewer>
</StackPanel>
</UserControl>