Here's the two chart:
Chart on the left is using class BarWithBorder : FrameworkElement
and the right one using class BarWithRect : UIElement
. The problem with the right one is I couldn't have rounded corners only on top and also couldn't scale up X
from its center. Another problem is when I resize the window the right one animate again because of drawing OnRender
, one more problem is when the Window is loaded I see bars on the right one before the animation. Here's BarWithRect
:
class BarWithRect : UIElement
{
SolidColorBrush brush;
Color normal, highlight;
ColorAnimation brushAnim;
RectAnimation rectAnim;
int value;
Rect rect;
public BarWithRect(int value) {
this.value = value;
normal = Colors.Gray;
highlight = Colors.Coral;
brush = new SolidColorBrush(normal);
brushAnim = new ColorAnimation() {
Duration = TimeSpan.FromSeconds(1),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }
};
rectAnim = new RectAnimation() {
BeginTime = TimeSpan.FromSeconds(0.75),
Duration = TimeSpan.FromSeconds(1),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseIn }
};
}
public void SetParameters(double width, double height, double upperBound) {
rect.Width = width - 5;
rect.Height = value / upperBound * height;
RenderSize = new Size(width, rect.Height);
rectAnim.From = new Rect(new Size(0, 0));
rectAnim.To = rect;
InvalidateVisual();
}
protected override void OnRender(DrawingContext dc) {
//dc.DrawRoundedRectangle(brush, null, rect, 10, 10);
dc.DrawRectangle(brush, null, rect, rectAnim.CreateClock());
}
protected override void OnMouseEnter(MouseEventArgs e) {
brushAnim.To = highlight;
brush.BeginAnimation(SolidColorBrush.ColorProperty, brushAnim);
}
protected override void OnMouseLeave(MouseEventArgs e) {
brushAnim.To = normal;
brush.BeginAnimation(SolidColorBrush.ColorProperty, brushAnim);
}
}
and here's the BarWithBorder
:
class BarWithBorder : FrameworkElement
{
SolidColorBrush brush;
Color normal, highlight;
Border border;
public double UpperBound;
ColorAnimation brushAnim;
ScaleTransform transform;
int value;
public BarWithBorder(int value) {
this.value = value;
normal = Colors.Gray;
highlight = Colors.Coral;
brush = new SolidColorBrush(normal);
transform = new ScaleTransform(0, 0);
border = new Border() {
Background = brush,
CornerRadius = new CornerRadius(0, 0, 10, 10),
Margin = new Thickness(5, 0, 0, 0),
RenderTransform = transform
};
AddVisualChild(border);
brushAnim = new ColorAnimation() {
Duration = TimeSpan.FromSeconds(1),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }
};
Loaded += animate;
}
void animate(object sender, RoutedEventArgs e) {
var borderScaleAnim = new DoubleAnimation() {
BeginTime = TimeSpan.FromSeconds(0.75),
To = 1,
Duration = TimeSpan.FromSeconds(1),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }
};
transform.BeginAnimation(ScaleTransform.ScaleXProperty, borderScaleAnim);
transform.BeginAnimation(ScaleTransform.ScaleYProperty, borderScaleAnim);
}
protected override Size MeasureOverride(Size availableSize) {
var width = availableSize.Width - border.Margin.Left;
transform.CenterX = width / 2;
border.Width = width;
border.Height = value / UpperBound * availableSize.Height;
border.Measure(availableSize);
return border.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize) {
border.Arrange(new Rect(border.DesiredSize));
return border.DesiredSize;
}
protected override void OnMouseEnter(MouseEventArgs e) {
brushAnim.To = highlight;
brush.BeginAnimation(SolidColorBrush.ColorProperty, brushAnim);
}
protected override void OnMouseLeave(MouseEventArgs e) {
brushAnim.To = normal;
brush.BeginAnimation(SolidColorBrush.ColorProperty, brushAnim);
}
protected override Visual GetVisualChild(int index) => border;
protected override int VisualChildrenCount => 1;
}
Both of these have been used in BarChart
, here it is:
class Barchart : FrameworkElement
{
double minValue, maxValue, horizontalOffset;
Size labelDesired;
VisualCollection Children;
public bool IsWithRect { get; set; }
public Barchart() {
Children = new VisualCollection(this);
ClipToBounds = true;
LayoutTransform = new ScaleTransform() { ScaleY = -1 };
}
void addLabel(string date) {
var label = new TextBlock() {
Tag = "Label",
Text = date,
IsHitTestVisible = false,
TextAlignment = TextAlignment.Right,
Padding = new Thickness(0, 0, 5, 0),
RenderTransform = new TransformGroup() {
Children = {
new ScaleTransform() { ScaleY = -1 },
new RotateTransform() { Angle = 90 }
}
}
};
label.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (labelDesired.Width < label.DesiredSize.Width)
labelDesired = label.DesiredSize;
Children.Add(label);
}
void addLine() {
var line = new Line() {
StrokeThickness = 1,
Stroke = Brushes.LightBlue,
IsHitTestVisible = false
};
Children.Add(line);
line.Loaded += animateLines;
}
void addTick(double value) {
var tick = new TextBlock() {
Text = value.ToString("N0"),
HorizontalAlignment = HorizontalAlignment.Right,
RenderTransform = new TransformGroup() {
Children = {
new ScaleTransform() { ScaleY = - 1 },
new TranslateTransform()
}
},
IsHitTestVisible = false
};
Children.Add(tick);
tick.Loaded += animateTicks;
}
void animateLabels(object sender, RoutedEventArgs e) {
var label = sender as Label;
var anim = new DoubleAnimationUsingKeyFrames() {
KeyFrames = {
new LinearDoubleKeyFrame(0, TimeSpan.FromSeconds(0)),
new LinearDoubleKeyFrame(0, TimeSpan.FromSeconds(4.5)),
new LinearDoubleKeyFrame(90, TimeSpan.FromSeconds(5.5))
}
};
var transform = (label.RenderTransform as TransformGroup).Children[1];
transform.BeginAnimation(RotateTransform.AngleProperty, anim);
}
void animateTicks(object sender, RoutedEventArgs e) {
var tick = sender as TextBlock;
var anim = new DoubleAnimationUsingKeyFrames() {
KeyFrames = {
new LinearDoubleKeyFrame(-30 - horizontalOffset, TimeSpan.FromSeconds(0)),
new LinearDoubleKeyFrame(-30 - horizontalOffset, TimeSpan.FromSeconds(0.5)),
new LinearDoubleKeyFrame(0, TimeSpan.FromSeconds(1)),
}
};
var transform = (tick.RenderTransform as TransformGroup).Children[1];
transform.BeginAnimation(TranslateTransform.XProperty, anim);
}
void animateLines(object sender, RoutedEventArgs e) {
var line = sender as Line;
var anim = new DoubleAnimationUsingKeyFrames() {
KeyFrames = {
new LinearDoubleKeyFrame(ActualHeight - line.Y1+15, TimeSpan.FromSeconds(0)),
new LinearDoubleKeyFrame(ActualHeight - line.Y1+15, TimeSpan.FromSeconds(0.5)),
new LinearDoubleKeyFrame(0, TimeSpan.FromSeconds(1)),
}
};
line.RenderTransform = new TranslateTransform(0, 0);
line.RenderTransform.BeginAnimation(TranslateTransform.YProperty, anim);
}
protected override Size ArrangeOverride(Size arrangeSize) {
var testTick = new TextBlock() { Text = maxValue.ToString("N0") };
testTick.Measure(arrangeSize);
double margin = 10;
var labelHeight = labelDesired.Width;
var tickSpace = horizontalOffset = testTick.DesiredSize.Width;
var barSpace = tickSpace;
var labelSpace = tickSpace;
var bars = IsWithRect ? Children.OfType<BarWithRect>().Count() : Children.OfType<BarWithBorder>().Count();
var barWidth = (arrangeSize.Width - tickSpace - margin) / bars;
var lineSpace = (arrangeSize.Height - labelHeight) / 10;
var y = labelHeight;
var textY = labelHeight;
var upperBound = maxValue;
var availableHeight = (arrangeSize.Height - labelHeight) / 10 * 9;
foreach (UIElement item in Children) {
if (item is BarWithBorder || item is BarWithRect) {
if (IsWithRect) {
var rect = item as BarWithRect;
rect.SetParameters(barWidth, availableHeight, upperBound);
rect.Arrange(new Rect(new Point(barSpace, labelHeight), rect.RenderSize));
barSpace += rect.RenderSize.Width;
}
else {
var rect = item as BarWithBorder;
rect.UpperBound = upperBound;
rect.Measure(new Size(barWidth, availableHeight));
rect.Arrange(new Rect(new Point(barSpace, labelHeight), rect.RenderSize));
barSpace += rect.DesiredSize.Width;
}
}
else if (item is Line) {
var line = item as Line;
line.X2 = arrangeSize.Width - margin;
line.Y1 = line.Y2 = y;
item.Measure(arrangeSize);
item.Arrange(new Rect(item.DesiredSize));
y += lineSpace;
}
else if (item is TextBlock) {
var block = item as TextBlock;
if (block.Tag != null) {
block.Width = labelDesired.Width;
block.Measure(arrangeSize);
block.Arrange(new Rect(new Point(labelSpace, 0), block.DesiredSize));
labelSpace += barWidth;
}
else {
block.Measure(arrangeSize);
block.Arrange(new Rect(new Point(0, textY + block.DesiredSize.Height), block.DesiredSize));
textY += lineSpace;
}
}
}
return arrangeSize;
}
protected override Visual GetVisualChild(int index) => Children[index];
protected override int VisualChildrenCount => Children.Count;
public IEnumerable ItemSource {
get { return (IEnumerable)GetValue(ItemSourceProperty); }
set { SetValue(ItemSourceProperty, value); }
}
public static readonly DependencyProperty ItemSourceProperty =
DependencyProperty.Register("ItemSource", typeof(IEnumerable), typeof(Barchart), new FrameworkPropertyMetadata() {
DefaultValue = null,
PropertyChangedCallback = onSourceChanged,
AffectsArrange = true
});
static void onSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var o = d as Barchart;
if (e.OldValue != null) o.Children.Clear();
o.labelDesired = new Size(0, 0);
o.maxValue = 0;
var list = (IEnumerable<int>)e.NewValue;
foreach (int value in list) {
if(o.IsWithRect) o.Children.Add(new BarWithRect(value));
else o.Children.Add(new BarWithBorder(value));
o.addLabel(DateTime.Today.ToString("dd MMM yyyy"));
if (value > o.maxValue) o.maxValue = value;
if (value < o.minValue) o.minValue = value;
}
var min = o.minValue < 0 ? o.minValue : 0;
double step = 0d;
var current = min;
if (min < 0) step = (o.maxValue + Math.Abs(min)) / 9;
else step = o.maxValue / 9;
for (int i = 0; i < 10; i++) {
o.addLine();
o.addTick(current);
current += step;
}
}
}
in the MainWindow.xaml
, I've these:
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<local:Barchart ItemSource="{Binding ChartValues}" Margin="10"/>
<local:Barchart Grid.Column="1" ItemSource="{Binding ChartValues}" IsWithRect="True" Margin="10"/>
</Grid>
and in MainWindow.xaml.cs
, these:
public partial class MainWindow : Window
{
public List<int> ChartValues { get; set; }
public MainWindow() {
InitializeComponent();
ChartValues = new List<int>();
Random rand = new();
for (int i = 0; i < 10; i++) {
ChartValues.Add(rand.Next(50, 501));
}
DataContext = this;
}
}
Can I make the right one look exactly like the left one and stop the animation when I resize window and hide that until the animation starts?