This is a migrated thread and some comments may be shown as answers.

Axis Title layout when binding text and font size to observable properties

3 Answers 367 Views
ChartView
This is a migrated thread and some comments may be shown as answers.
Thilo
Top achievements
Rank 1
Thilo asked on 08 May 2017, 03:52 PM

Hello,

I added axis titles to my chart by adding TextBlocks. When providing the Text and FontSize properties through constant literal values immediately within the xaml code, everything works fine. The plot area is reduced in order to leave enough room for the titles, and the titles itself are centered on the axes' extents.

However, when using bindings in order to provide the values for the Text and FontSize properties from a view model (INotifyPropertyChanged, standard observable properties), the plot area is not reduced to accommodate the titles, resulting in the titles being rendered off-center and either partially or fully outside the chart control's bounds.

I have created a minimal demonstration program, which consists of a main window xaml definition containing the chart:

<Window x:Class="WpfApplication2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"   xmlns:wpfApplication2="clr-namespace:WpfApplication2"   Title="MainWindow" Height="600" Width="800"><DockPanel LastChildFill="True"><telerik:RadCartesianChart Margin="100" x:Name="mChart" BorderBrush="Aquamarine" BorderThickness="5" ClipToBounds="False"><telerik:RadCartesianChart.DataContext><wpfApplication2:ChartViewModel/></telerik:RadCartesianChart.DataContext><telerik:LineSeries StrokeThickness="5"><telerik:LineSeries.DataPoints><telerik:CategoricalDataPoint Category="A" Value="2" /><telerik:CategoricalDataPoint Category="B" Value="5" /><telerik:CategoricalDataPoint Category="C" Value="3" /></telerik:LineSeries.DataPoints></telerik:LineSeries><telerik:RadCartesianChart.HorizontalAxis><telerik:CategoricalAxis><telerik:CategoricalAxis.Title><TextBlock Text="{Binding TitleText}" FontSize="{Binding FontSize, Mode=OneWay}"/></telerik:CategoricalAxis.Title></telerik:CategoricalAxis></telerik:RadCartesianChart.HorizontalAxis><telerik:RadCartesianChart.VerticalAxis><telerik:LinearAxis><telerik:LinearAxis.Title><TextBlock Text="{Binding TitleText}" FontSize="{Binding FontSize, Mode=OneWay}"/></telerik:LinearAxis.Title></telerik:LinearAxis></telerik:RadCartesianChart.VerticalAxis></telerik:RadCartesianChart></DockPanel></Window>

 

The Viewmodel is trivial:

using System.ComponentModel;
using System.Runtime.CompilerServices;
using WpfApplication2.Annotations;
 
namespace WpfApplication2
{
    class ChartViewModel : INotifyPropertyChanged
    {
        #region Fields
 
        private string mTitleText;
        public string TitleText { get { return mTitleText; } set { SetProperty(ref mTitleText, value); } }
 
        private double mFontSize;
        public double FontSize { get { return mFontSize; } set { SetProperty(ref mFontSize, value); } }
 
        #endregion
 
        #region Construction
 
        public ChartViewModel()
        {
            mTitleText = "THIS IS THE AXIS TITLE";
            mFontSize = 72.0d;
        }
 
        #endregion
 
        #region Interface
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        #endregion
 
        #region Implementation
 
        protected bool SetProperty<TProperty>(ref TProperty aStorage, TProperty aValue, [CallerMemberName]string aPropertyName = null)
        {
            if (Equals(aStorage, aValue))
                return false;
 
            aStorage = aValue;
 
            // ReSharper disable ExplicitCallerInfoArgument
            RaisePropertyChanged(aPropertyName);
            // ReSharper restore ExplicitCallerInfoArgument
 
            return true;
        }
 
        public bool SetProperty<TProperty>(ref TProperty aStorage, TProperty aValue, ref bool aChangedFlag, [CallerMemberName]string aPropertyName = null)
        {
            // ReSharper disable once ExplicitCallerInfoArgument
            if (!SetProperty(ref aStorage, aValue, aPropertyName))
                return false;
 
            aChangedFlag = true;
            return true;
        }
 
        protected void RaiseOtherPropertyChanged(string aPropertyName)
        {
            // ReSharper disable once ExplicitCallerInfoArgument
            RaisePropertyChanged(aPropertyName);
        }
 
        [NotifyPropertyChangedInvocator]
        protected virtual void RaisePropertyChanged([CallerMemberName] string aPropertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(aPropertyName));
        }
 
        #endregion
    }
}

 

The attached screenshot shows the result. Please note that the aquamarine rectangle is the actual border of the chart, and that the ClipToBounds property is set to false, otherwise the title of the categorical axis would not be visible at all.

My goal is to allow the user to design the axes' titles' appearances dynamically, not only be defining their text, but also their font, font size, weight, etc. Therefore the according properties of the TextBlocks will have to be changed dynamically at run-time. Is there any other way to achieve this without breaking the layout mechanism?

Any help would be greatly appreciated, thanks in advance!

3 Answers, 1 is accepted

Sort by
0
Thilo
Top achievements
Rank 1
answered on 09 May 2017, 09:16 AM

Hello again,

a similar problem seems to exist in relation to the tick labels of the categorical axis. As soon as the TextBox's FontSize property is bound to an observable property in the view model, the layouting mechanism does not seem to work properly.

Please see the attached screenshot for a demonstration. The chart on the left side does not use any bindings, the right side does.

MainWindow.xaml:

<Window x:Class="WpfApplication2.MainWindow"
        xmlns:wpfApplication2="clr-namespace:WpfApplication2"
        Title="MainWindow" Height="600" Width="800">
 
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
 
        <!-- static -->
        <DockPanel Grid.Column="0" LastChildFill="True">
            <telerik:RadCartesianChart Margin="100" BorderBrush="Aquamarine" BorderThickness="5" ClipToBounds="False">
 
                <telerik:BarSeries>
                    <telerik:BarSeries.DataPoints>
                        <telerik:CategoricalDataPoint Category="Aardvarks" Value="2" />
                        <telerik:CategoricalDataPoint Category="Giraffes" Value="5" />
                        <telerik:CategoricalDataPoint Category="Duck Billed Platypusses" Value="3" />
                    </telerik:BarSeries.DataPoints>
                </telerik:BarSeries>
 
                <telerik:RadCartesianChart.HorizontalAxis>
                    <telerik:CategoricalAxis LabelFitMode="Rotate">
                        <telerik:CategoricalAxis.Title>
                            <TextBlock Text="Species" FontSize="40"/>
                        </telerik:CategoricalAxis.Title>
                        <telerik:CategoricalAxis.LabelTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding}" FontSize="20"/>
                            </DataTemplate>
                        </telerik:CategoricalAxis.LabelTemplate>
                    </telerik:CategoricalAxis>
                </telerik:RadCartesianChart.HorizontalAxis>
 
                <telerik:RadCartesianChart.VerticalAxis>
                    <telerik:LinearAxis>
                        <telerik:LinearAxis.Title>
                            <TextBlock Text="Naps per Day" FontSize="40"/>
                        </telerik:LinearAxis.Title>
                    </telerik:LinearAxis>
                </telerik:RadCartesianChart.VerticalAxis>
 
            </telerik:RadCartesianChart>
 
        </DockPanel>
 
        <!-- bindings -->
        <DockPanel Grid.Column="1" LastChildFill="True">
            <telerik:RadCartesianChart Margin="100" BorderBrush="Aquamarine" BorderThickness="5" ClipToBounds="False">
 
                <telerik:RadCartesianChart.DataContext>
                    <wpfApplication2:ChartViewModel/>
                </telerik:RadCartesianChart.DataContext>
 
                <telerik:BarSeries>
                    <telerik:BarSeries.DataPoints>
                        <telerik:CategoricalDataPoint Category="Aardvarks" Value="2" />
                        <telerik:CategoricalDataPoint Category="Giraffes" Value="5" />
                        <telerik:CategoricalDataPoint Category="Duck Billed Platypusses" Value="3" />
                    </telerik:BarSeries.DataPoints>
                </telerik:BarSeries>
 
                <telerik:RadCartesianChart.HorizontalAxis>
                    <telerik:CategoricalAxis LabelFitMode="Rotate">
                        <telerik:CategoricalAxis.Title>
                            <TextBlock Text="{Binding XAxisTitleText, Mode=OneWay}" FontSize="{Binding AxisTitleFontSize, Mode=OneWay}"/>
                        </telerik:CategoricalAxis.Title>
                        <telerik:CategoricalAxis.LabelTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding}" FontSize="{Binding RelativeSource={RelativeSource AncestorType={x:Type telerik:RadCartesianChart}}, Path=DataContext.TickLabelFontSize, Mode=OneWay}"/>
                            </DataTemplate>
                        </telerik:CategoricalAxis.LabelTemplate>
                    </telerik:CategoricalAxis>
                </telerik:RadCartesianChart.HorizontalAxis>
 
                <telerik:RadCartesianChart.VerticalAxis>
                    <telerik:LinearAxis>
                        <telerik:LinearAxis.Title>
                            <TextBlock Text="{Binding YAxisTitleText, Mode=OneWay}" FontSize="{Binding AxisTitleFontSize, Mode=OneWay}"/>
                        </telerik:LinearAxis.Title>
                    </telerik:LinearAxis>
                </telerik:RadCartesianChart.VerticalAxis>
 
            </telerik:RadCartesianChart>
 
        </DockPanel>
    </Grid>
</Window>

ChartViewModel.cs:

using System.ComponentModel;
using System.Runtime.CompilerServices;
using WpfApplication2.Annotations;
 
namespace WpfApplication2
{
    class ChartViewModel : INotifyPropertyChanged
    {
        #region Fields
 
        private string mXAxisTitleText;
        public string XAxisTitleText { get { return mXAxisTitleText; } set { SetProperty(ref mXAxisTitleText, value); } }
 
        private string mYAxisTitleText;
        public string YAxisTitleText { get { return mYAxisTitleText; } set { SetProperty(ref mYAxisTitleText, value); } }
 
        private double mAxisTitleFontSize;
        public double AxisTitleFontSize { get { return mAxisTitleFontSize; } set { SetProperty(ref mAxisTitleFontSize, value); } }
 
        private double mTickLabelFontSize;
        public double TickLabelFontSize { get { return mTickLabelFontSize; } set { SetProperty(ref mTickLabelFontSize, value); } }
 
        private double mDataLabelFontSize;
        public double DataLabelFontSize { get { return mDataLabelFontSize; } set { SetProperty(ref mDataLabelFontSize, value); } }
 
        #endregion
 
        #region Construction
 
        public ChartViewModel()
        {
            mXAxisTitleText = "Species";
            mYAxisTitleText = "Naps per Day";
            mAxisTitleFontSize = 40.0d;
            mTickLabelFontSize = 20.0d;
            mDataLabelFontSize = 20.0d;
        }
 
        #endregion
 
        #region Interface
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        #endregion
 
        #region Implementation
 
        protected bool SetProperty<TProperty>(ref TProperty aStorage, TProperty aValue, [CallerMemberName]string aPropertyName = null)
        {
            if (Equals(aStorage, aValue))
                return false;
 
            aStorage = aValue;
 
            // ReSharper disable ExplicitCallerInfoArgument
            RaisePropertyChanged(aPropertyName);
            // ReSharper restore ExplicitCallerInfoArgument
 
            return true;
        }
 
        [NotifyPropertyChangedInvocator]
        protected virtual void RaisePropertyChanged([CallerMemberName] string aPropertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(aPropertyName));
        }
 
        #endregion
    }
}

 

Again, any help would be greatly appreciated. Many thanks in advance!
0
Accepted
Martin Ivanov
Telerik team
answered on 11 May 2017, 04:05 PM
Hello Thilo,

This behavior comes from a performance optimization in the chart. Basically, the sizes of all its label presenters defined via DataTemplates as the LabelTemplate for example, are cached when the chart is loaded. Any change in the size of the element in the template won't be respected by the parent content presenter. 

So, what happens basically is that the chart loads. The value from the binding is not yet set. The chart sees that the TextBlock doesn't have any text so it measures its presenter with 0,0. After this the binding evaluates, the text comes and the TextBlock is measured again by the WPF framework. However, the chart doesn't change the parent presenter's size. 

To change this behavior you can reset the Title or the LabelTemplate of the chart. For example, after you set the tick labels values you can reset the LabelTemplate of the axis in code. Specifically for the LabelTemplate you can try using ElementName binding instead of RelativeSource.

Regards,
Martin
Telerik by Progress
Want to extend the target reach of your WPF applications, leveraging iOS, Android, and UWP? Try UI for Xamarin, a suite of polished and feature-rich components for the Xamarin framework, which allow you to write beautiful native mobile apps using a single shared C# codebase.
0
Thilo
Top achievements
Rank 1
answered on 12 May 2017, 09:35 AM

Hello Martin,

thank you for your answer, it put me on the right track.

Tags
ChartView
Asked by
Thilo
Top achievements
Rank 1
Answers by
Thilo
Top achievements
Rank 1
Martin Ivanov
Telerik team
Share this question
or