Creating your own Chart Piece

You can create a custom piece and apply it to a series without adjusting the core core or even having the source code of the other charts in your application. (you do need the .dll of course!)
This is only for rendering the data point piece OR the whole series - to change the axis or layout the source code is still required. The information below may still be uiseful if you want to create / alter your own code within the source code.

Each piece requires
  • Class whose parent class is GAMultiPiece. This is the class that can draw and redraw or move a piece.
  • A style whose target type is this class. This provides the control template, and sets things such as the animation between states.
  • A style for the unselected piece.
  • A style for the selected piece, if required.
  • A style for the series legend.

Example of control template for the Column Piece

  • The below style is standard style for the Column Piece and could be used as a template.
  • The VisualStateGroups define the transitions between the UnselectedPiece and the SelectionHighlight.
  • The actual design of the piece is placed within a grid. This is the suggested method but us not strictly require. The line piece is just a canvas. In this case the grid has 3 rows, the top used for text, the middle used for the element, and the third used for text.
  • The 1st and 3rd grids have their visibility set in the class code, so that the top text (black) is only seen if there is not enough room for the lower text (white).
  • The 'Slice' is referenced in the class you defined and its properties altered based on the value of the datapoint (this will be more obvious when the code is seen).
  • The values available provided by the datapoint class. Some of the useful ones are
Value The actual value of the datapoint
FormattedValue The value formatted for a mouse-over tooltip
GADataPointStyle The style for the datapoint - supplied by the Series
GASelectedDataPointStyle The style for the datapoint when its selected - supplied by the Series


    <Style TargetType="local:GAColumnPiece">
        <Setter Property="VerticalAlignment" Value="Bottom" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:GAColumnPiece">
                    <Grid >
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:0.1" />
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="PointerOver">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="MouseOverHighlight" Storyboard.TargetProperty="Opacity" To="0.6" Duration="0" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="MouseOver">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="MouseOverHighlight" Storyboard.TargetProperty="Opacity" To="0.6" Duration="0" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="SelectionStates">
                                <VisualState x:Name="Unselected">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="SelectionHighlight" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.2" />
                                        <DoubleAnimation Storyboard.TargetName="UnselectedPiece" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.0" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Selected">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="SelectionHighlight" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.2" />
                                        <DoubleAnimation Storyboard.TargetName="UnselectedPiece" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.3" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*" />
                            <RowDefinition Height="auto" />
                            <RowDefinition Height="*" />
                        </Grid.RowDefinitions>
                        <local:AutoSizeTextBlock Grid.Row="0" 
                                                 Text="{Binding Path=Value}" TextBlockStyle="{StaticResource NumberStyle}" VerticalAlignment="Top"
                                                 HorizontalAlignment="Stretch"
                                                 Name="TopNumber">
                        </local:AutoSizeTextBlock>
                        <Border x:Name="Slice"
                        	Background="Transparent" 
                            Grid.Row="1"  
                            Width="auto" Height="auto" MinWidth="2" 
                            ToolTipService.ToolTip="{Binding Path=FormattedValue}" >
                            
                            <Grid Background="Transparent">
                                <Rectangle x:Name="UnselectedPiece" Style="{Binding GADataPointStyle}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
                                <Rectangle x:Name="SelectionHighlight" Style="{Binding GASelectedDataPointStyle}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Opacity="0"/> 
                                <local:AutoSizeTextBlock x:Name="MainTextBlock" Text="{Binding Path=Value}"
                                                         TextBlockStyle="{StaticResource NumberStyle}" 
                                                         VerticalAlignment="{Binding IsNegativePiece,RelativeSource={RelativeSource Mode=TemplatedParent},Converter={StaticResource IsNegativeToVerticalAlignmentConverter}}"
                                                         Foreground="White" 
                                                         HorizontalAlignment="Stretch" />
                            </Grid>
                        </Border>
                        <local:AutoSizeTextBlock Grid.Row="2"
                                                 Text="{Binding Path=Value}" 
                                                 TextBlockStyle="{StaticResource NumberStyle}"
                                                 VerticalAlignment="Top" 
                                                 HorizontalAlignment="Stretch"
                                                 Name="BottomNumber">
                        </local:AutoSizeTextBlock>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Example of the class code for the Column Piece

  • In the protected override void InternalOnApplyTemplate() you should reference any UI objects you may need.
  • Use the protected override void DrawGeometry() to draw the piece. In this case it sets and runs an animation to alert the height of the rectangl object. At the start of the code you should check for UI elements==null and return if this is the case.
  • There are two mehtods you can create or override that I found useful in writing my code.
    • void DataPointGroup_PropertyChanged(object sender, PropertyChangedEventArgs e)
      • In the GALine code I have created one group for all the datapoints and whenever the group is changed then this is called. I use this to redraw an individual point.
    • private static void OnPercentageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      • This is generally set in the code to call the drawing code when an indiviaual point changes. This could be exactly the same code that is called to initially draw the piece, or it could be different.

namespace GravityApps.Mandelkow.MetroCharts
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Text;
    using System.Windows;  
    using System.Reflection;
    using System.Collections.Specialized;
    using System.Windows.Input;
    using System.Windows.Data;

#if NETFX_CORE
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Media;
    using Windows.UI.Xaml.Shapes;
    using Windows.UI.Xaml.Markup;
    using Windows.UI.Xaml;
    using Windows.Foundation;
    using Windows.UI;
    using Windows.UI.Xaml.Media.Animation;
    using Windows.UI.Core;
#else
    using System.Windows.Media;
    using System.Windows.Controls;
    using System.Windows.Media.Animation;
    using System.Windows.Shapes;
    
#endif

    public class GAColumnPiece : GAMultiPiece
    {


        private Border slice = null;
        bool isNegativePiece = false;
        AutoSizeTextBlock bottomText = null;
        AutoSizeTextBlock topText = null;

        public static readonly DependencyProperty TextExceedsHeightProperty =
           DependencyProperty.Register("TextExceedsHeight", typeof(bool), typeof(GAColumnPiece),
           new PropertyMetadata(false, new PropertyChangedCallback(OnPercentageChanged)));

        public bool TextExceedsHeight
        {
            get { return (bool)GetValue(TextExceedsHeightProperty); }
            set { SetValue(TextExceedsHeightProperty, value); }
        }

        #region Constructors

        static GAColumnPiece()        
        {
#if NETFX_CORE
                        
#elif SILVERLIGHT
    
#else
            DefaultStyleKeyProperty.OverrideMetadata(typeof(GAColumnPiece), new FrameworkPropertyMetadata(typeof(GAColumnPiece)));
#endif
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="GAColumnPiece"/> class.
        /// </summary>
        public GAColumnPiece()
        {
#if NETFX_CORE
            this.DefaultStyleKey = typeof(GAColumnPiece);
#endif
#if SILVERLIGHT
            this.DefaultStyleKey = typeof(GAColumnPiece);
#endif
            Loaded += ColumnPiece_Loaded;
        }

        #endregion Constructors



        #region Methods

        public override Style GetDefaultStyle()
        {
            object o = TryFindResource("GAColumnPieceStyle");
            return (Style)o;
        }

        public override Style GetDefaultSelectedStyle()
        {
            object o = TryFindResource("GAColumnPieceSelectedStyle");
            return (Style)o;
        }

        public override Style GetDefaultLegendStyle()
        {
            object o = TryFindResource("GAColumnPieceLegendStyle");
            return (Style)o;
        }

        private static void OnPercentageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as GAColumnPiece).DrawGeometry();
        }

        protected override void InternalOnApplyTemplate()
        {
            slice = this.GetTemplateChild("Slice") as Border;
            RegisterMouseEvents(slice);
            setup();
        }


        private void setup()
        {
            bottomText = this.GetTemplateChild("BottomNumber") as AutoSizeTextBlock;
            topText = this.GetTemplateChild("TopNumber") as AutoSizeTextBlock;
            
            Grid mainGrid = (Grid)this.Parent;
            GAMultiPiece piece = (GAMultiPiece)mainGrid.TemplatedParent;
            isNegativePiece = piece.Name == "NegativeMultiPiece";
            AutoSizeTextBlock MainTextBlock = this.GetTemplateChild("MainTextBlock") as AutoSizeTextBlock; ;

            // Bind mainTextBox IsHeightExceedsSpaceProperty to this TextExceedsHeight
            var mainTextBlockBinding = new Binding();
            mainTextBlockBinding.Source = this;
            mainTextBlockBinding.Mode = BindingMode.OneWayToSource;
            mainTextBlockBinding.Path = new PropertyPath("TextExceedsHeight");
            BindingOperations.SetBinding(MainTextBlock, AutoSizeTextBlock.IsHeightExceedsSpaceProperty, mainTextBlockBinding);

        }

        void ColumnPiece_Loaded(object sender, RoutedEventArgs e)
        {
            DrawGeometry();
        }


        protected override void DrawGeometry(bool withAnimation = true)
        {
            if (slice == null) return;
            try
            {
                if (this.ClientWidth <= 0.0)
                {
                    return;
                }
                if (this.ClientHeight <= 0.0)
                {
                    return;
                }

                if (this.Percentage < 0)
                {
                    this.Visibility = Visibility.Collapsed;
                    return;
                }
                else
                {
                    this.Visibility = Visibility.Visible;
                }

                if (bottomText!=null)
                {
                    bottomText.Visibility = getBottomTextVisibilty(Percentage, isNegativePiece, TextExceedsHeight);
                }

                if (topText != null)
                {
                    topText.Visibility = getTopTextVisibilty(Percentage, isNegativePiece, TextExceedsHeight);
                }

                double endHeight = ClientHeight;
                double percentToUse = Percentage;

                double startHeight = 0;
                if (slice.Height > 0)
                {
                    startHeight = slice.Height;
                }

                if (percentToUse < 0) percentToUse = 0;
                endHeight = endHeight * percentToUse;

                DoubleAnimation scaleAnimation = new DoubleAnimation();
                scaleAnimation.From = startHeight;
                scaleAnimation.To = endHeight;

                scaleAnimation.Duration = TimeSpan.FromMilliseconds(withAnimation ? 500: 0);
                scaleAnimation.EasingFunction = new QuarticEase() { EasingMode = EasingMode.EaseOut };
                Storyboard storyScaleX = new Storyboard();
                storyScaleX.Children.Add(scaleAnimation);

                Storyboard.SetTarget(storyScaleX, slice);

#if NETFX_CORE
                scaleAnimation.EnableDependentAnimation = true;
                Storyboard.SetTargetProperty(storyScaleX, "Height");
#else
                Storyboard.SetTargetProperty(storyScaleX, new PropertyPath("Height"));
#endif
                storyScaleX.Begin();
            }
            catch (Exception ex)
            {
            }
        }

        private Visibility getBottomTextVisibilty(double Percentage, bool isNegativePiece, bool heightExceedsSpace)
        {
            if (isNegativePiece && heightExceedsSpace && Percentage!=0) return Visibility.Visible;
            return Visibility.Collapsed;
        }

        private Visibility getTopTextVisibilty(double Percentage, bool isNegativePiece, bool heightExceedsSpace)
        {
            if (!isNegativePiece && Percentage == 0) return Visibility.Visible;
            if (!isNegativePiece && heightExceedsSpace) return Visibility.Visible;
            return Visibility.Collapsed;
        }

        #endregion Methods
    }
}

Last edited Oct 8, 2015 at 2:31 AM by gravityapps, version 1