After looking at several examples of creating piechart, I've made the following UserControl
for that:
public partial class PieChart : UserControl
{
#region DependencyProperties
public static readonly DependencyProperty RadiusProperty = DependencyProperty.Register("Radius", typeof(double), typeof(PieChart), new PropertyMetadata(0d));
public static readonly DependencyProperty SeriesProperty = DependencyProperty.Register("Series", typeof(List<PieSeries>), typeof(PieChart), new PropertyMetadata(null, Draw));
public double Radius {
get { return (double)GetValue(RadiusProperty); }
set { SetValue(RadiusProperty, value); }
}
public List<PieSeries> Series {
get { return (List<PieSeries>)GetValue(SeriesProperty); }
set { SetValue(SeriesProperty, value); }
}
static void Draw(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as PieChart;
control.AddPie(control.chartArea);
}
#endregion
Brush[] colors = new Brush[] { Brushes.Gray, Brushes.Green, Brushes.Blue, Brushes.LightGray, Brushes.AntiqueWhite };
double xCenter, yCenter;
public PieChart()
{
InitializeComponent();
}
void AddPie(Canvas canvas)
{
canvas.Width = 300; canvas.Height = 300;
xCenter = canvas.Width / 2;
yCenter = canvas.Height / 2;
double sum, startAngle, sweepAngle;
sum = Series.Sum(x => x.Value);
startAngle = sweepAngle = 0.0;
for (int i = 0; i < Series.Count; i++)
{
var brush = colors[i];
startAngle += sweepAngle;
sweepAngle = 2 * Math.PI * Series[i].Value / sum;
DrawSegments(canvas, brush, startAngle, startAngle + sweepAngle);
}
}
void DrawSegments(Canvas canvas, Brush fillColor, double startAngle, double endAngle)
{
var line1 = new LineSegment() { Point = new Point(xCenter + Radius * Math.Cos(startAngle), yCenter + Radius * Math.Sin(startAngle)) };
var line2 = new LineSegment() { Point = new Point(xCenter + Radius * Math.Cos(endAngle), yCenter + Radius * Math.Sin(endAngle)) };
var arc = new ArcSegment()
{
SweepDirection = SweepDirection.Clockwise,
Size = new Size(Radius, Radius),
Point = new Point(xCenter + Radius * Math.Cos(endAngle), yCenter + Radius * Math.Sin(endAngle))
};
var figure = new PathFigure() { IsClosed = true, StartPoint = new Point(xCenter, yCenter), Segments = { line1, arc, line2 } };
var geometry = new PathGeometry() { Figures = { figure } };
var path = new Path() { Fill = fillColor, Data = geometry };
canvas.Children.Add(path);
}
}
with this in xaml
:
<UserControl ...>
<Grid>
<Canvas x:Name="chartArea" Margin="10"/>
</Grid>
</UserControl>
not sure whether this is the right way to do that BUT it works. The problem with this is in AddPie
method. I've to set the Width
and Height
of the Canvas
, otherwise nothing shows up in MainWindow
. Here's how I've used it in MainWindow
:
<Window>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:PieChart Grid.Row="1"
Radius="100"
Series="{Binding Series}"/>
</Grid>
</Window>
and in the constructor
of ViewModel, I've created the Series
:
Series = new List<PieSeries>()
{
new PieSeries("A", 30),
new PieSeries("B", 20),
new PieSeries("C", 10),
new PieSeries("D", 15),
new PieSeries("E", 25)
};
in Draw
callback I always get 0
as ActualHeight
and ActualWidth
of that Canvas
, named chartArea
, so the Pie doesn't resize automatically when I resize the window!
How to fix that?
Is there any better way to Draw simple pie chart? In my case List<PieSeries>
may have 1 item to 5 items in it.
EDIT
With the approach suggested in the comments, it's much simpler! In my ViewModel, VM
, I've these:
public class VM : INotifyPropertyChanged
{
public ObservableCollection<ShapeData> Series { get; set; } = new ObservableCollection<ShapeData>();
double[] array = { 30, 10, 15, 20, 25};
Brush[] brushes = new Brush[] { Brushes.Gray, Brushes.Green, Brushes.Blue, Brushes.LightGray, Brushes.AntiqueWhite };
double radius, xCenter, yCenter;
public Command Resized { get; set; }
public VM()
{
radius = 100;
Resized = new Command(resized, (o) => true);
}
void resized(object obj)
{
var canvas = obj as Canvas;
xCenter = canvas.ActualWidth / 2;
yCenter = canvas.ActualHeight / 2;
Series.Clear();
DrawPie(array, brushes, radius);
}
void DrawPie(double[] values, Brush[] colors, double radius)
{
var sum = values.Sum();
double startAngle, sweepAngle;
startAngle = sweepAngle = 0;
for (int i = 0; i < values.Length; i++)
{
startAngle += sweepAngle;
sweepAngle = 2 * Math.PI * values[i] / sum;
var line1 = new LineSegment() { Point = new Point(xCenter + radius * Math.Cos(startAngle), yCenter + radius * Math.Sin(startAngle)) };
var line2 = new LineSegment() { Point = new Point(xCenter + radius * Math.Cos(startAngle + sweepAngle), yCenter + radius * Math.Sin(startAngle + sweepAngle)) };
var arc = new ArcSegment()
{
SweepDirection = SweepDirection.Clockwise,
Size = new Size(radius, radius),
Point = new Point(xCenter + radius * Math.Cos(startAngle + sweepAngle), yCenter + radius * Math.Sin(startAngle + sweepAngle))
};
var figure = new PathFigure() { IsClosed = true, StartPoint = new Point(xCenter, yCenter), Segments = { line1, arc, line2 } };
Series.Add(new ShapeData()
{
Geometry = new PathGeometry() { Figures = { figure } },
Fill = colors[i],
Stroke = Brushes.Red,
StrokeThickness = 1
});
}
}
#region Notify Property Changed Members
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
#endregion
}
and in xaml
:
<ItemsControl Grid.Row="1" ItemsSource="{Binding Series}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path Data="{Binding Geometry}"
Fill="{Binding Fill}"
Stroke="{Binding Stroke}"
StrokeThickness="{Binding StrokeThickness}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="panel">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SizeChanged">
<i:InvokeCommandAction Command="{Binding Resized}"
CommandParameter="{Binding ElementName=panel}"/>
</i:EventTrigger>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding Resized}"
CommandParameter="{Binding ElementName=panel}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
with this in xaml
, I get an warning on <ItemsPanelTemplate>
tag
No default constructor found for type 'System.Windows.Interactivity.TriggerCollection'. You can use the Arguments or FactoryMethod directives to construct this type.