I've a PieChart, a custom Panel, and it adds a number of PieSlice, a custom FrameworkElement, based on the number of int
in the IEnumerable
I provide as its ItemSource
, a DependecnyProperty. When it adds slices it chooses color randomly. Sometimes those colors are pleasant and sometimes those hurt my eyes. Here's the two relevant lines in the PropertyChangedCallback of PieChart's ItemsSource:
var color = Color.FromRgb((byte)rand.Next(0, 256), (byte)rand.Next(0, 256), (byte)rand.Next(0, 256));
var slice = new PieSlice(new SolidColorBrush(color), item);
that sometimes hurts! When the app is loaded it looks like this:
one more issue I've at the point where PieSlices meet. Looks like there's stripe in between slices and its apparent when the colors are light. Here's the code for PieChart
:
public class PieChart : Panel
{
Popup pop;
TextBlock value, percentage;
Path ellipse;
int total;
DoubleAnimation ellipseAnim;
BooleanAnimationUsingKeyFrames popOpenAnim, popCloseAnim;
public PieChart() {
Margin = new Thickness(10);
value = new TextBlock() {
FontSize = 32,
FontWeight = FontWeights.Bold,
Foreground = Brushes.Gray,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center
};
percentage = new TextBlock() {
Foreground = Brushes.Blue,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center
};
pop = new Popup() {
AllowsTransparency = true,
PlacementTarget = this,
Placement = PlacementMode.Center,
Child = new StackPanel() {
Children = {
value,
new Separator(),
percentage
}
}
};
ellipseAnim = new DoubleAnimation() {
BeginTime = TimeSpan.FromSeconds(1),
Duration = TimeSpan.FromSeconds(1),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut },
};
popOpenAnim = new BooleanAnimationUsingKeyFrames() {
KeyFrames = {
new DiscreteBooleanKeyFrame(false, TimeSpan.FromSeconds(0)),
new DiscreteBooleanKeyFrame(true, TimeSpan.FromSeconds(1.5)),
}
};
popCloseAnim = new BooleanAnimationUsingKeyFrames() {
KeyFrames = {
new DiscreteBooleanKeyFrame(false, TimeSpan.FromSeconds(1))
}
};
}
protected override Size ArrangeOverride(Size finalSize) {
if (ItemSource != null) {
var list = ItemSource.Cast<int>().ToList();
double cx = finalSize.Width / 2;
double cy = finalSize.Height / 2;
double radius, startAngle, sweepAngle, endAngle;
startAngle = sweepAngle = endAngle = 0d;
radius = cx > cy ? cy : cx;
var total = list.Sum();
int index = 0;
foreach (PieSlice item in Children.OfType<PieSlice>()) {
var value = list[index];
startAngle += sweepAngle;
sweepAngle = 2 * Math.PI * value / total;
endAngle = startAngle + sweepAngle;
bool isLarge = (double)value / total > 0.5;
item.SetParameters(cx, cy, radius, startAngle, sweepAngle, isLarge);
item.Arrange(new Rect(item.DesiredSize));
index++;
}
ellipse = Children.OfType<Path>().First();
var geo = ellipse.Data as EllipseGeometry;
geo.RadiusX = geo.RadiusY = radius - 100;
geo.Center = new Point(cx, cy);
ellipse.Measure(finalSize);
ellipse.Arrange(new Rect(ellipse.DesiredSize));
ellipse.RenderTransform = new ScaleTransform(0, 0);
}
return finalSize;
}
void animateEllipse(double scale) {
double x, y, cx, cy;
cx = ActualWidth / 2;
cy = ActualHeight / 2;
if (scale == 1) {
x = y = 0;
pop.BeginAnimation(Popup.IsOpenProperty, popOpenAnim);
}
else {
var transfrom = ellipse.RenderTransform as ScaleTransform;
x = transfrom.ScaleX;
y = transfrom.ScaleY;
pop.BeginAnimation(Popup.IsOpenProperty, popCloseAnim);
}
ellipse.RenderTransform = new ScaleTransform(x, y, cx, cy);
ellipseAnim.To = scale;
ellipse.RenderTransform.BeginAnimation(ScaleTransform.ScaleXProperty, ellipseAnim);
ellipse.RenderTransform.BeginAnimation(ScaleTransform.ScaleYProperty, ellipseAnim);
}
void showPopup(object sender, MouseEventArgs e) {
double val = (sender as PieSlice).value;
value.Text = val.ToString();
percentage.Text = (val / total * 100).ToString("N2") + " %";
animateEllipse(1);
}
void hidePopup(object sender, MouseEventArgs e) => animateEllipse(0);
public IEnumerable ItemSource {
get { return (IEnumerable)GetValue(ItemSourceProperty); }
set { SetValue(ItemSourceProperty, value); }
}
public static readonly DependencyProperty ItemSourceProperty =
DependencyProperty.Register("ItemSource", typeof(IEnumerable), typeof(PieChart), new PropertyMetadata(null, onSourceChanged));
static void onSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var o = d as PieChart;
if (e.OldValue != null) o.Children.Clear();
if (o.ItemSource != null) {
Random rand = new();
foreach (int item in o.ItemSource) {
o.total += item;
var color = Color.FromRgb((byte)rand.Next(0, 256), (byte)rand.Next(0, 256), (byte)rand.Next(0, 256));
var slice = new PieSlice(new SolidColorBrush(color), item);
o.Children.Add(slice);
slice.MouseEnter += o.showPopup;
slice.MouseLeave += o.hidePopup;
}
var path = new Path() {
Fill = Brushes.White,
Data = new EllipseGeometry()
};
o.Children.Add(path);
}
}
}
and here's PieSlice
:
public class PieSlice : FrameworkElement
{
PathGeometry geometry, arcGeo;
PathFigure figure;
LineSegment startLine;
ArcSegment arc;
SolidColorBrush original, background;
DoubleAnimation translateX, translateY, scaleAndRotate;
ColorAnimation colorAnim;
double cx, dx, cy, dy, radius, rotation;
Point start, end;
bool isLargeArc;
public int value;
public PieSlice(SolidColorBrush brush, int value) {
this.value = value;
original = brush;
background = new SolidColorBrush(original.Color);
startLine = new LineSegment();
arc = new ArcSegment() { SweepDirection = SweepDirection.Clockwise };
figure = new PathFigure() {
IsClosed = true,
Segments = { startLine, arc }
};
geometry = new PathGeometry() { Figures = { figure } };
Loaded += animateSlice;
initAnimations();
}
void initAnimations() {
scaleAndRotate = new DoubleAnimation() {
From = 0,
Duration = TimeSpan.FromSeconds(3),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }
};
translateX = new DoubleAnimation() {
Duration = TimeSpan.FromSeconds(2),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut }
};
translateY = new DoubleAnimation() {
Duration = TimeSpan.FromSeconds(2),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut }
};
colorAnim = new ColorAnimation() {
Duration = TimeSpan.FromSeconds(2),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }
};
}
void animateSlice(object sender, RoutedEventArgs e) {
arcGeo.Freeze();
var arcPointAnim = new PointAnimationUsingPath() {
PathGeometry = arcGeo,
Duration = TimeSpan.FromSeconds(2),
FillBehavior = FillBehavior.Stop
};
RenderTransform = new TransformGroup() {
Children = {
new ScaleTransform(1, 1) {
CenterX = cx,
CenterY = cy
},
new RotateTransform(360) {
CenterX = cx,
CenterY = cy
}
}
};
var scale = (RenderTransform as TransformGroup).Children[0];
var rotate = (RenderTransform as TransformGroup).Children[1];
scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAndRotate);
scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAndRotate);
rotate.BeginAnimation(RotateTransform.AngleProperty, scaleAndRotate);
rotate.Changed += updateAngle;
arc.BeginAnimation(ArcSegment.PointProperty, arcPointAnim);
}
void updateAngle(object sender, EventArgs e) {
rotation = (sender as RotateTransform).Angle;
if (rotation == 360) {
RenderTransform = new TranslateTransform(0, 0);
}
}
public void SetParameters(double cx, double cy, double radius, double startAngle, double sweepAngle, bool isLargeArc) {
this.cx = cx;
this.cy = cy;
this.radius = radius;
this.isLargeArc = isLargeArc;
start = new Point(cx + radius * Math.Cos(startAngle), cy + radius * Math.Sin(startAngle));
end = new Point(cx + radius * Math.Cos(startAngle + sweepAngle), cy + radius * Math.Sin(startAngle + sweepAngle));
dx = 10 * Math.Cos(startAngle + sweepAngle / 2);
dy = 10 * Math.Sin(startAngle + sweepAngle / 2);
arcGeo = new PathGeometry() {
Figures = {
new PathFigure() {
StartPoint = start,
Segments = {
new ArcSegment() {
SweepDirection = SweepDirection.Clockwise,
Point = end,
Size = new Size(radius, radius),
IsLargeArc = isLargeArc
}
}
}
}
};
Draw();
}
void Draw() {
figure.StartPoint = new Point(cx, cy);
startLine.Point = start;
arc.Point = end;
arc.Size = new Size(radius, radius);
arc.IsLargeArc = isLargeArc;
}
protected override void OnRender(DrawingContext dc) => dc.DrawGeometry(background, null, geometry);
protected override void OnMouseEnter(MouseEventArgs e) {
if (rotation < 360) return;
var t = (RenderTransform as TranslateTransform);
translateX.By = dx - t.X;
translateY.By = dy - t.Y;
RenderTransform.BeginAnimation(TranslateTransform.XProperty, translateX);
RenderTransform.BeginAnimation(TranslateTransform.YProperty, translateY);
colorAnim.To = Colors.Gray;
background.BeginAnimation(SolidColorBrush.ColorProperty, colorAnim);
}
protected override void OnMouseLeave(MouseEventArgs e) {
if (rotation < 360) return;
var t = (RenderTransform as TranslateTransform);
translateX.By = -t.X;
translateY.By = -t.Y;
RenderTransform.BeginAnimation(TranslateTransform.XProperty, translateX);
RenderTransform.BeginAnimation(TranslateTransform.YProperty, translateY);
colorAnim.To = original.Color;
background.BeginAnimation(SolidColorBrush.ColorProperty, colorAnim);
}
}
just add PieChart
in MainWindow.xaml
and give it a List<int>/ObservableCollection<int>
as its ItemSource
and you'll be able to see it clearly. In the PieSlice I've an ArcSegment and a LineSegment at start. I've tried by adding another LineSegment at the end BUT I see those stripes.