0

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.

Clemens
  • 123,504
  • 12
  • 155
  • 268
  • 1
    Just a note, when `chartArea` is a member of PieChart, it seems pointless to pass it as argument to the AddPie method. Also, when you write `var control = d as PieChart` you should check the result for `null`. Better use an explicit cast like `var control = (PieChart)d;` because it would correctly give you an InvalidCastException (instead of an incorrect NullReferenceException) in case d is not a PieChart. – Clemens Dec 12 '19 at 12:51
  • 1
    Besides that, the `Series` property is set before layout is performed. You control needs to redraw itself when its size changes. – Clemens Dec 12 '19 at 12:55
  • @Clemens, that static `Draw` requires an instance. –  Dec 12 '19 at 12:55
  • 1
    AddPie is not a static method. – Clemens Dec 12 '19 at 12:57
  • @Clemens, is it a must to have a `SizeChanged` handler? –  Dec 12 '19 at 12:57
  • 1
    I'd override OnRenderSizeChanged. – Clemens Dec 12 '19 at 12:57
  • @Clemens, visual studio doesn't let me call non static method in a static method and it doesn't even let me pass `chartArea` as an argument even if I make `AddPie` static. It says object reference is required! –  Dec 12 '19 at 13:02
  • 1
    To me it seems also odd that you create and add those Paths manually. You already know how an ItemsControl works. Just use a Canvas as its ItemsPanel, and a Path in the ItemTemplate, which just has its Data property bound to a Geometry in the ItemsSource collection. – Clemens Dec 12 '19 at 13:02
  • 1
    For your previous comment, you already call `control.AddPie`. No need to pass `control.chartArea` as argument. Inside AddPie, you can access the chartArea member because - as said - AddPie is not a static method. – Clemens Dec 12 '19 at 13:03
  • @Clemens, that's where (ItemsControl) I got stuck! Since it looks odd asking a question without working example, I've posted this! I actually don't want to use this. I couldn't figure out how to do that with `ItemsControl`. Felt like I've to have one Converter to figure out the center point for `PathFigure`, two more for 2 `LineSegment` and one last for `ArcSegment`. Could you please provide an example with `ItemsControl`? –  Dec 12 '19 at 13:27
  • 1
    Start with an ItemsControl with a Canvas as ItemsPanel, that has its ItemsSource set to an `ObservableCollection` or an ObservableCollection of an item class with a Geometry property and a Brush property. Something like this: https://stackoverflow.com/a/40190793/1136211 – Clemens Dec 12 '19 at 13:29
  • @Clemens, in that case I still have to compute `line1`, `line2`, `arc` and `figure` manually, like I did in this example, to add those in `Geometry` property of `ShapeData`, right? –  Dec 12 '19 at 13:44
  • 1
    Yes, of course. You may perhap translate the Canvas by half the width and height of your control, in order to have the Geometry coordinate system origin at (0,0) – Clemens Dec 12 '19 at 13:45
  • @Clemens, yes that works BUT the Pie is in top left corner and 3 quarters of it got cut off! Is there a simple way to translate origin of canvas, or I've to have a value converter for that? –  Dec 12 '19 at 14:06
  • A RenderTransform or Margin should do. – Clemens Dec 12 '19 at 14:27
  • @Clemens, I've used event triggers, see edit part. –  Dec 12 '19 at 14:35

0 Answers0